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.InetAddress;
20 import java.nio.ByteBuffer;
21 import java.text.SimpleDateFormat;
22 import java.util.Calendar;
24 import java.util.Map.Entry;
25 import java.util.TimeZone;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import org.apache.commons.lang.StringUtils;
30 import org.openhab.binding.keba.internal.KebaBindingConstants.KebaSeries;
31 import org.openhab.binding.keba.internal.KebaBindingConstants.KebaType;
32 import org.openhab.core.cache.ExpiringCacheMap;
33 import org.openhab.core.config.core.Configuration;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.IncreaseDecreaseType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.PercentType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.binding.BaseThingHandler;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.openhab.core.types.State;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
51 import com.google.gson.JsonElement;
52 import com.google.gson.JsonObject;
53 import com.google.gson.JsonParseException;
54 import com.google.gson.JsonParser;
57 * The {@link KeContactHandler} is responsible for handling commands, which
58 * are sent to one of the channels.
60 * @author Karel Goderis - Initial contribution
62 public class KeContactHandler extends BaseThingHandler {
64 public static final String IP_ADDRESS = "ipAddress";
65 public static final String POLLING_REFRESH_INTERVAL = "refreshInterval";
66 public static final int REPORT_INTERVAL = 3000;
67 public static final int PING_TIME_OUT = 3000;
68 public static final int BUFFER_SIZE = 1024;
69 public static final int REMOTE_PORT_NUMBER = 7090;
70 private static final String CACHE_REPORT_1 = "REPORT_1";
71 private static final String CACHE_REPORT_2 = "REPORT_2";
72 private static final String CACHE_REPORT_3 = "REPORT_3";
73 private static final String CACHE_REPORT_100 = "REPORT_100";
75 private final Logger logger = LoggerFactory.getLogger(KeContactHandler.class);
77 protected final JsonParser parser = new JsonParser();
78 private final KeContactTransceiver transceiver;
80 private ScheduledFuture<?> pollingJob;
81 private ExpiringCacheMap<String, ByteBuffer> cache;
83 private int maxPresetCurrent = 0;
84 private int maxSystemCurrent = 63000;
85 private KebaType type;
86 private KebaSeries series;
87 private int lastState = -1; // trigger a report100 at startup
88 private boolean isReport100needed = true;
90 public KeContactHandler(Thing thing, KeContactTransceiver transceiver) {
92 this.transceiver = transceiver;
96 public void initialize() {
97 if (getConfig().get(IP_ADDRESS) != null && !getConfig().get(IP_ADDRESS).equals("")) {
98 transceiver.registerHandler(this);
100 cache = new ExpiringCacheMap<>(
101 Math.max((((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue()) - 5, 0) * 1000);
103 cache.put(CACHE_REPORT_1, () -> transceiver.send("report 1", getHandler()));
104 cache.put(CACHE_REPORT_2, () -> transceiver.send("report 2", getHandler()));
105 cache.put(CACHE_REPORT_3, () -> transceiver.send("report 3", getHandler()));
106 cache.put(CACHE_REPORT_100, () -> transceiver.send("report 100", getHandler()));
108 if (pollingJob == null || pollingJob.isCancelled()) {
110 pollingJob = scheduler.scheduleWithFixedDelay(this::pollingRunnable, 0,
111 ((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue(), TimeUnit.SECONDS);
112 } catch (Exception e) {
113 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
114 "An exception occurred while scheduling the polling job");
118 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
119 "IP address or port number not set");
124 public void dispose() {
125 if (pollingJob != null && !pollingJob.isCancelled()) {
126 pollingJob.cancel(true);
130 transceiver.unRegisterHandler(this);
133 public String getIPAddress() {
134 return getConfig().get(IP_ADDRESS) != null ? (String) getConfig().get(IP_ADDRESS) : "";
137 private KeContactHandler getHandler() {
142 public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, String description) {
143 super.updateStatus(status, statusDetail, description);
147 protected Configuration getConfig() {
148 return super.getConfig();
151 private void pollingRunnable() {
153 long stamp = System.currentTimeMillis();
154 if (!InetAddress.getByName(((String) getConfig().get(IP_ADDRESS))).isReachable(PING_TIME_OUT)) {
155 logger.debug("Ping timed out after '{}' milliseconds", System.currentTimeMillis() - stamp);
156 transceiver.unRegisterHandler(getHandler());
158 if (getThing().getStatus() == ThingStatus.ONLINE) {
159 ByteBuffer response = cache.get(CACHE_REPORT_1);
160 if (response != null) {
164 Thread.sleep(REPORT_INTERVAL);
166 response = cache.get(CACHE_REPORT_2);
167 if (response != null) {
171 Thread.sleep(REPORT_INTERVAL);
173 response = cache.get(CACHE_REPORT_3);
174 if (response != null) {
178 if (isReport100needed) {
179 Thread.sleep(REPORT_INTERVAL);
181 response = cache.get(CACHE_REPORT_100);
182 if (response != null) {
185 isReport100needed = false;
189 } catch (NumberFormatException | IOException e) {
190 logger.debug("An exception occurred while polling the KEBA KeContact '{}': {}", getThing().getUID(),
192 Thread.currentThread().interrupt();
193 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194 "An exception occurred while while polling the charging station");
195 } catch (InterruptedException e) {
196 logger.debug("Polling job has been interrupted for handler of thing '{}'.", getThing().getUID());
200 protected void onData(ByteBuffer byteBuffer) {
201 String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
202 response = StringUtils.chomp(response);
204 if (response.contains("TCH-OK")) {
205 // ignore confirmation messages which are not JSON
210 JsonObject readObject = parser.parse(response).getAsJsonObject();
212 for (Entry<String, JsonElement> entry : readObject.entrySet()) {
213 switch (entry.getKey()) {
215 Map<String, String> properties = editProperties();
216 String product = entry.getValue().getAsString().trim();
217 properties.put(CHANNEL_MODEL, product);
218 updateProperties(properties);
219 if (product.contains("P20")) {
221 } else if (product.contains("P30")) {
224 series = KebaSeries.getSeries(product.substring(13, 14).charAt(0));
228 Map<String, String> properties = editProperties();
229 properties.put(CHANNEL_SERIAL, entry.getValue().getAsString());
230 updateProperties(properties);
234 Map<String, String> properties = editProperties();
235 properties.put(CHANNEL_FIRMWARE, entry.getValue().getAsString());
236 updateProperties(properties);
240 int state = entry.getValue().getAsInt();
243 updateState(CHANNEL_WALLBOX, OnOffType.OFF);
244 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
245 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
249 updateState(CHANNEL_WALLBOX, OnOffType.ON);
250 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
251 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
255 updateState(CHANNEL_WALLBOX, OnOffType.ON);
256 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
257 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
261 updateState(CHANNEL_WALLBOX, OnOffType.ON);
262 updateState(CHANNEL_VEHICLE, OnOffType.ON);
263 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
267 updateState(CHANNEL_WALLBOX, OnOffType.ON);
268 updateState(CHANNEL_VEHICLE, OnOffType.ON);
269 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
276 int state = entry.getValue().getAsInt();
277 State newState = new DecimalType(state);
278 updateState(CHANNEL_STATE, newState);
279 if (lastState != state) {
280 // the state is different from the last one, so we will trigger a report100
281 isReport100needed = true;
287 int state = entry.getValue().getAsInt();
290 updateState(CHANNEL_ENABLED, OnOffType.ON);
294 updateState(CHANNEL_ENABLED, OnOffType.OFF);
301 int state = entry.getValue().getAsInt();
302 maxSystemCurrent = state;
303 State newState = new DecimalType(state);
304 updateState(CHANNEL_MAX_SYSTEM_CURRENT, newState);
305 if (maxSystemCurrent != 0) {
306 if (maxSystemCurrent < maxPresetCurrent) {
307 transceiver.send("curr " + String.valueOf(maxSystemCurrent), this);
308 updateState(CHANNEL_MAX_PRESET_CURRENT, new DecimalType(maxSystemCurrent));
309 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE,
310 new PercentType((maxSystemCurrent - 6000) * 100 / (maxSystemCurrent - 6000)));
313 logger.debug("maxSystemCurrent is 0. Ignoring.");
318 int state = entry.getValue().getAsInt();
319 maxPresetCurrent = state;
320 updateState(CHANNEL_MAX_PRESET_CURRENT, new DecimalType(state));
321 if (maxSystemCurrent != 0) {
322 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE,
323 new PercentType(Math.min(100, (state - 6000) * 100 / (maxSystemCurrent - 6000))));
328 int state = entry.getValue().getAsInt();
329 State newState = new DecimalType(state);
330 updateState(CHANNEL_FAILSAFE_CURRENT, newState);
334 int state = entry.getValue().getAsInt();
335 maxPresetCurrent = state;
336 updateState(CHANNEL_PILOT_CURRENT, new DecimalType(state));
337 updateState(CHANNEL_PILOT_PWM, new DecimalType(state));
341 int state = entry.getValue().getAsInt();
344 updateState(CHANNEL_OUTPUT, OnOffType.ON);
348 updateState(CHANNEL_OUTPUT, OnOffType.OFF);
355 int state = entry.getValue().getAsInt();
358 updateState(CHANNEL_INPUT, OnOffType.ON);
362 updateState(CHANNEL_INPUT, OnOffType.OFF);
369 long state = entry.getValue().getAsLong();
371 Calendar uptime = Calendar.getInstance();
372 uptime.setTimeZone(TimeZone.getTimeZone("GMT"));
373 uptime.setTimeInMillis(state * 1000);
374 SimpleDateFormat pFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
375 pFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
377 updateState(CHANNEL_UPTIME, new DateTimeType(pFormatter.format(uptime.getTime())));
381 int state = entry.getValue().getAsInt();
382 State newState = new DecimalType(state);
383 updateState(CHANNEL_U1, newState);
387 int state = entry.getValue().getAsInt();
388 State newState = new DecimalType(state);
389 updateState(CHANNEL_U2, newState);
393 int state = entry.getValue().getAsInt();
394 State newState = new DecimalType(state);
395 updateState(CHANNEL_U3, newState);
399 int state = entry.getValue().getAsInt();
400 State newState = new DecimalType(state);
401 updateState(CHANNEL_I1, newState);
405 int state = entry.getValue().getAsInt();
406 State newState = new DecimalType(state);
407 updateState(CHANNEL_I2, newState);
411 int state = entry.getValue().getAsInt();
412 State newState = new DecimalType(state);
413 updateState(CHANNEL_I3, newState);
417 long state = entry.getValue().getAsLong();
418 State newState = new DecimalType(state / 1000);
419 updateState(CHANNEL_POWER, newState);
423 int state = entry.getValue().getAsInt();
424 State newState = new PercentType(state / 10);
425 updateState(CHANNEL_POWER_FACTOR, newState);
429 long state = entry.getValue().getAsLong();
430 State newState = new DecimalType(state / 10);
431 updateState(CHANNEL_SESSION_CONSUMPTION, newState);
435 long state = entry.getValue().getAsLong();
436 State newState = new DecimalType(state / 10);
437 updateState(CHANNEL_TOTAL_CONSUMPTION, newState);
441 int state = entry.getValue().getAsInt();
442 State newState = new DecimalType(state);
443 updateState(CHANNEL_AUTHON, newState);
447 int state = entry.getValue().getAsInt();
448 State newState = new DecimalType(state);
449 updateState(CHANNEL_AUTHREQ, newState);
453 String state = entry.getValue().getAsString().trim();
454 State newState = new StringType(state);
455 updateState(CHANNEL_SESSION_RFID_TAG, newState);
459 String state = entry.getValue().getAsString().trim();
460 State newState = new StringType(state);
461 updateState(CHANNEL_SESSION_RFID_CLASS, newState);
465 int state = entry.getValue().getAsInt();
466 State newState = new DecimalType(state);
467 updateState(CHANNEL_SESSION_SESSION_ID, newState);
471 int state = entry.getValue().getAsInt() / 10;
472 State newState = new DecimalType(state);
473 updateState(CHANNEL_SETENERGY, newState);
478 } catch (JsonParseException e) {
479 logger.debug("Invalid JSON data will be ignored: '{}'", response);
484 public void handleCommand(ChannelUID channelUID, Command command) {
485 if ((command instanceof RefreshType)) {
486 // let's assume we do frequent enough polling and ignore the REFRESH request here
487 // in order to prevent too many channel state updates
489 switch (channelUID.getId()) {
490 case CHANNEL_MAX_PRESET_CURRENT: {
491 if (command instanceof DecimalType) {
493 "curr " + String.valueOf(
494 Math.min(Math.max(6000, ((DecimalType) command).intValue()), maxSystemCurrent)),
499 case CHANNEL_MAX_PRESET_CURRENT_RANGE: {
500 if (command instanceof OnOffType || command instanceof IncreaseDecreaseType
501 || command instanceof PercentType) {
503 if (command == IncreaseDecreaseType.INCREASE) {
504 newValue = Math.min(Math.max(6000, maxPresetCurrent + 1), maxSystemCurrent);
505 } else if (command == IncreaseDecreaseType.DECREASE) {
506 newValue = Math.min(Math.max(6000, maxPresetCurrent - 1), maxSystemCurrent);
507 } else if (command == OnOffType.ON) {
508 newValue = maxSystemCurrent;
509 } else if (command == OnOffType.OFF) {
511 } else if (command instanceof PercentType) {
512 newValue = 6000 + (maxSystemCurrent - 6000) * ((PercentType) command).intValue() / 100;
517 transceiver.send("curr " + String.valueOf(newValue), this);
521 case CHANNEL_ENABLED: {
522 if (command instanceof OnOffType) {
523 if (command == OnOffType.ON) {
524 transceiver.send("ena 1", this);
525 } else if (command == OnOffType.OFF) {
526 transceiver.send("ena 0", this);
533 case CHANNEL_OUTPUT: {
534 if (command instanceof OnOffType) {
535 if (command == OnOffType.ON) {
536 transceiver.send("output 1", this);
537 } else if (command == OnOffType.OFF) {
538 transceiver.send("output 0", this);
545 case CHANNEL_DISPLAY: {
546 if (command instanceof StringType) {
547 if (type == KebaType.P30 && (series == KebaSeries.C || series == KebaSeries.X)) {
548 String cmd = command.toString();
549 int maxLength = (cmd.length() < 23) ? cmd.length() : 23;
550 transceiver.send("display 0 0 0 0 " + cmd.substring(0, maxLength), this);
552 logger.warn("'Display' is not supported on a KEBA KeContact {}:{}", type, series);
557 case CHANNEL_SETENERGY: {
558 if (command instanceof DecimalType) {
560 "setenergy " + String.valueOf(
561 Math.min(Math.max(0, ((DecimalType) command).intValue() * 10), 999999999)),
566 case CHANNEL_AUTHENTICATE: {
567 if (command instanceof StringType) {
568 String cmd = command.toString();
569 // cmd must contain ID + CLASS (works only if the RFID TAG is in the whitelist of the Keba
571 transceiver.send("start " + cmd, this);