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 JsonParser parser = new JsonParser();
79 private ScheduledFuture<?> pollingJob;
80 private static KeContactTransceiver transceiver = new KeContactTransceiver();
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) {
95 public void initialize() {
96 if (getConfig().get(IP_ADDRESS) != null && !getConfig().get(IP_ADDRESS).equals("")) {
97 transceiver.registerHandler(this);
99 cache = new ExpiringCacheMap<>(
100 Math.max((((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue()) - 5, 0) * 1000);
102 cache.put(CACHE_REPORT_1, () -> transceiver.send("report 1", getHandler()));
103 cache.put(CACHE_REPORT_2, () -> transceiver.send("report 2", getHandler()));
104 cache.put(CACHE_REPORT_3, () -> transceiver.send("report 3", getHandler()));
105 cache.put(CACHE_REPORT_100, () -> transceiver.send("report 100", getHandler()));
107 if (pollingJob == null || pollingJob.isCancelled()) {
109 pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0,
110 ((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue(), TimeUnit.SECONDS);
111 } catch (Exception e) {
112 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
113 "An exception occurred while scheduling the polling job");
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
118 "IP address or port number not set");
123 public void dispose() {
124 if (pollingJob != null && !pollingJob.isCancelled()) {
125 pollingJob.cancel(true);
129 transceiver.unRegisterHandler(this);
132 public String getIPAddress() {
133 return getConfig().get(IP_ADDRESS) != null ? (String) getConfig().get(IP_ADDRESS) : "";
136 private KeContactHandler getHandler() {
141 public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, String description) {
142 super.updateStatus(status, statusDetail, description);
146 protected Configuration getConfig() {
147 return super.getConfig();
150 private Runnable pollingRunnable = () -> {
152 long stamp = System.currentTimeMillis();
153 if (!InetAddress.getByName(((String) getConfig().get(IP_ADDRESS))).isReachable(PING_TIME_OUT)) {
154 logger.debug("Ping timed out after '{}' milliseconds", System.currentTimeMillis() - stamp);
155 transceiver.unRegisterHandler(getHandler());
157 if (getThing().getStatus() == ThingStatus.ONLINE) {
158 ByteBuffer response = cache.get(CACHE_REPORT_1);
159 if (response != null) {
163 Thread.sleep(REPORT_INTERVAL);
165 response = cache.get(CACHE_REPORT_2);
166 if (response != null) {
170 Thread.sleep(REPORT_INTERVAL);
172 response = cache.get(CACHE_REPORT_3);
173 if (response != null) {
177 if (isReport100needed) {
178 Thread.sleep(REPORT_INTERVAL);
180 response = cache.get(CACHE_REPORT_100);
181 if (response != null) {
184 isReport100needed = false;
188 } catch (NumberFormatException | IOException e) {
189 logger.debug("An exception occurred while polling the KEBA KeContact '{}': {}", getThing().getUID(),
191 Thread.currentThread().interrupt();
192 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
193 "An exception occurred while while polling the charging station");
194 } catch (InterruptedException e) {
195 logger.debug("Polling job has been interrupted for handler of thing '{}'.", getThing().getUID());
199 protected void onData(ByteBuffer byteBuffer) {
200 String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
201 response = StringUtils.chomp(response);
203 if (response.contains("TCH-OK")) {
204 // ignore confirmation messages which are not JSON
209 JsonObject readObject = parser.parse(response).getAsJsonObject();
211 for (Entry<String, JsonElement> entry : readObject.entrySet()) {
212 switch (entry.getKey()) {
214 Map<String, String> properties = editProperties();
215 String product = entry.getValue().getAsString().trim();
216 properties.put(CHANNEL_MODEL, product);
217 updateProperties(properties);
218 if (product.contains("P20")) {
220 } else if (product.contains("P30")) {
223 series = KebaSeries.getSeries(product.substring(13, 14).charAt(0));
227 Map<String, String> properties = editProperties();
228 properties.put(CHANNEL_SERIAL, entry.getValue().getAsString());
229 updateProperties(properties);
233 Map<String, String> properties = editProperties();
234 properties.put(CHANNEL_FIRMWARE, entry.getValue().getAsString());
235 updateProperties(properties);
239 int state = entry.getValue().getAsInt();
242 updateState(CHANNEL_WALLBOX, OnOffType.OFF);
243 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
244 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
248 updateState(CHANNEL_WALLBOX, OnOffType.ON);
249 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
250 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
254 updateState(CHANNEL_WALLBOX, OnOffType.ON);
255 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
256 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
260 updateState(CHANNEL_WALLBOX, OnOffType.ON);
261 updateState(CHANNEL_VEHICLE, OnOffType.ON);
262 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
266 updateState(CHANNEL_WALLBOX, OnOffType.ON);
267 updateState(CHANNEL_VEHICLE, OnOffType.ON);
268 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
275 int state = entry.getValue().getAsInt();
276 State newState = new DecimalType(state);
277 updateState(CHANNEL_STATE, newState);
278 if (lastState != state) {
279 // the state is different from the last one, so we will trigger a report100
280 isReport100needed = true;
286 int state = entry.getValue().getAsInt();
289 updateState(CHANNEL_ENABLED, OnOffType.ON);
293 updateState(CHANNEL_ENABLED, OnOffType.OFF);
300 int state = entry.getValue().getAsInt();
301 maxSystemCurrent = state;
302 State newState = new DecimalType(state);
303 updateState(CHANNEL_MAX_SYSTEM_CURRENT, newState);
304 if (maxSystemCurrent != 0) {
305 if (maxSystemCurrent < maxPresetCurrent) {
306 transceiver.send("curr " + String.valueOf(maxSystemCurrent), this);
307 updateState(CHANNEL_MAX_PRESET_CURRENT, new DecimalType(maxSystemCurrent));
308 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE,
309 new PercentType((maxSystemCurrent - 6000) * 100 / (maxSystemCurrent - 6000)));
312 logger.debug("maxSystemCurrent is 0. Ignoring.");
317 int state = entry.getValue().getAsInt();
318 maxPresetCurrent = state;
319 updateState(CHANNEL_MAX_PRESET_CURRENT, new DecimalType(state));
320 if (maxSystemCurrent != 0) {
321 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE,
322 new PercentType(Math.min(100, (state - 6000) * 100 / (maxSystemCurrent - 6000))));
327 int state = entry.getValue().getAsInt();
328 State newState = new DecimalType(state);
329 updateState(CHANNEL_FAILSAFE_CURRENT, newState);
333 int state = entry.getValue().getAsInt();
334 maxPresetCurrent = state;
335 updateState(CHANNEL_PILOT_CURRENT, new DecimalType(state));
336 updateState(CHANNEL_PILOT_PWM, new DecimalType(state));
340 int state = entry.getValue().getAsInt();
343 updateState(CHANNEL_OUTPUT, OnOffType.ON);
347 updateState(CHANNEL_OUTPUT, OnOffType.OFF);
354 int state = entry.getValue().getAsInt();
357 updateState(CHANNEL_INPUT, OnOffType.ON);
361 updateState(CHANNEL_INPUT, OnOffType.OFF);
368 long state = entry.getValue().getAsLong();
370 Calendar uptime = Calendar.getInstance();
371 uptime.setTimeZone(TimeZone.getTimeZone("GMT"));
372 uptime.setTimeInMillis(state * 1000);
373 SimpleDateFormat pFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
374 pFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
376 updateState(CHANNEL_UPTIME, new DateTimeType(pFormatter.format(uptime.getTime())));
380 int state = entry.getValue().getAsInt();
381 State newState = new DecimalType(state);
382 updateState(CHANNEL_U1, newState);
386 int state = entry.getValue().getAsInt();
387 State newState = new DecimalType(state);
388 updateState(CHANNEL_U2, newState);
392 int state = entry.getValue().getAsInt();
393 State newState = new DecimalType(state);
394 updateState(CHANNEL_U3, newState);
398 int state = entry.getValue().getAsInt();
399 State newState = new DecimalType(state);
400 updateState(CHANNEL_I1, newState);
404 int state = entry.getValue().getAsInt();
405 State newState = new DecimalType(state);
406 updateState(CHANNEL_I2, newState);
410 int state = entry.getValue().getAsInt();
411 State newState = new DecimalType(state);
412 updateState(CHANNEL_I3, newState);
416 long state = entry.getValue().getAsLong();
417 State newState = new DecimalType(state / 1000);
418 updateState(CHANNEL_POWER, newState);
422 int state = entry.getValue().getAsInt();
423 State newState = new PercentType(state / 10);
424 updateState(CHANNEL_POWER_FACTOR, newState);
428 long state = entry.getValue().getAsLong();
429 State newState = new DecimalType(state / 10);
430 updateState(CHANNEL_SESSION_CONSUMPTION, newState);
434 long state = entry.getValue().getAsLong();
435 State newState = new DecimalType(state / 10);
436 updateState(CHANNEL_TOTAL_CONSUMPTION, newState);
440 int state = entry.getValue().getAsInt();
441 State newState = new DecimalType(state);
442 updateState(CHANNEL_AUTHON, newState);
446 int state = entry.getValue().getAsInt();
447 State newState = new DecimalType(state);
448 updateState(CHANNEL_AUTHREQ, newState);
452 String state = entry.getValue().getAsString().trim();
453 State newState = new StringType(state);
454 updateState(CHANNEL_SESSION_RFID_TAG, newState);
458 String state = entry.getValue().getAsString().trim();
459 State newState = new StringType(state);
460 updateState(CHANNEL_SESSION_RFID_CLASS, newState);
464 int state = entry.getValue().getAsInt();
465 State newState = new DecimalType(state);
466 updateState(CHANNEL_SESSION_SESSION_ID, newState);
470 int state = entry.getValue().getAsInt() / 10;
471 State newState = new DecimalType(state);
472 updateState(CHANNEL_SETENERGY, newState);
477 } catch (JsonParseException e) {
478 logger.debug("Invalid JSON data will be ignored: '{}'", response);
483 public void handleCommand(ChannelUID channelUID, Command command) {
484 if ((command instanceof RefreshType)) {
485 // let's assume we do frequent enough polling and ignore the REFRESH request here
486 // in order to prevent too many channel state updates
488 switch (channelUID.getId()) {
489 case CHANNEL_MAX_PRESET_CURRENT: {
490 if (command instanceof DecimalType) {
492 "curr " + String.valueOf(
493 Math.min(Math.max(6000, ((DecimalType) command).intValue()), maxSystemCurrent)),
498 case CHANNEL_MAX_PRESET_CURRENT_RANGE: {
499 if (command instanceof OnOffType || command instanceof IncreaseDecreaseType
500 || command instanceof PercentType) {
502 if (command == IncreaseDecreaseType.INCREASE) {
503 newValue = Math.min(Math.max(6000, maxPresetCurrent + 1), maxSystemCurrent);
504 } else if (command == IncreaseDecreaseType.DECREASE) {
505 newValue = Math.min(Math.max(6000, maxPresetCurrent - 1), maxSystemCurrent);
506 } else if (command == OnOffType.ON) {
507 newValue = maxSystemCurrent;
508 } else if (command == OnOffType.OFF) {
510 } else if (command instanceof PercentType) {
511 newValue = 6000 + (maxSystemCurrent - 6000) * ((PercentType) command).intValue() / 100;
516 transceiver.send("curr " + String.valueOf(newValue), this);
520 case CHANNEL_ENABLED: {
521 if (command instanceof OnOffType) {
522 if (command == OnOffType.ON) {
523 transceiver.send("ena 1", this);
524 } else if (command == OnOffType.OFF) {
525 transceiver.send("ena 0", this);
532 case CHANNEL_OUTPUT: {
533 if (command instanceof OnOffType) {
534 if (command == OnOffType.ON) {
535 transceiver.send("output 1", this);
536 } else if (command == OnOffType.OFF) {
537 transceiver.send("output 0", this);
544 case CHANNEL_DISPLAY: {
545 if (command instanceof StringType) {
546 if (type == KebaType.P30 && (series == KebaSeries.C || series == KebaSeries.X)) {
547 String cmd = command.toString();
548 int maxLength = (cmd.length() < 23) ? cmd.length() : 23;
549 transceiver.send("display 0 0 0 0 " + cmd.substring(0, maxLength), this);
551 logger.warn("'Display' is not supported on a KEBA KeContact {}:{}", type, series);
556 case CHANNEL_SETENERGY: {
557 if (command instanceof DecimalType) {
559 "setenergy " + String.valueOf(
560 Math.min(Math.max(0, ((DecimalType) command).intValue() * 10), 999999999)),
565 case CHANNEL_AUTHENTICATE: {
566 if (command instanceof StringType) {
567 String cmd = command.toString();
568 // cmd must contain ID + CLASS (works only if the RFID TAG is in the whitelist of the Keba
570 transceiver.send("start " + cmd, this);