]> git.basschouten.com Git - openhab-addons.git/blob
3e96e38393d4f41fb9fc013b62f354db2b5b6878
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.keba.internal.handler;
14
15 import static org.openhab.binding.keba.internal.KebaBindingConstants.*;
16
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;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.TimeZone;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28
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;
50
51 import com.google.gson.JsonElement;
52 import com.google.gson.JsonObject;
53 import com.google.gson.JsonParseException;
54 import com.google.gson.JsonParser;
55
56 /**
57  * The {@link KeContactHandler} is responsible for handling commands, which
58  * are sent to one of the channels.
59  *
60  * @author Karel Goderis - Initial contribution
61  */
62 public class KeContactHandler extends BaseThingHandler {
63
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";
74
75     private final Logger logger = LoggerFactory.getLogger(KeContactHandler.class);
76
77     protected JsonParser parser = new JsonParser();
78
79     private ScheduledFuture<?> pollingJob;
80     private static KeContactTransceiver transceiver = new KeContactTransceiver();
81     private ExpiringCacheMap<String, ByteBuffer> cache;
82
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;
89
90     public KeContactHandler(Thing thing) {
91         super(thing);
92     }
93
94     @Override
95     public void initialize() {
96         if (getConfig().get(IP_ADDRESS) != null && !getConfig().get(IP_ADDRESS).equals("")) {
97             transceiver.registerHandler(this);
98
99             cache = new ExpiringCacheMap<>(
100                     Math.max((((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue()) - 5, 0) * 1000);
101
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()));
106
107             if (pollingJob == null || pollingJob.isCancelled()) {
108                 try {
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");
114                 }
115             }
116         } else {
117             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
118                     "IP address or port number not set");
119         }
120     }
121
122     @Override
123     public void dispose() {
124         if (pollingJob != null && !pollingJob.isCancelled()) {
125             pollingJob.cancel(true);
126             pollingJob = null;
127         }
128
129         transceiver.unRegisterHandler(this);
130     }
131
132     public String getIPAddress() {
133         return getConfig().get(IP_ADDRESS) != null ? (String) getConfig().get(IP_ADDRESS) : "";
134     }
135
136     private KeContactHandler getHandler() {
137         return this;
138     }
139
140     @Override
141     public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, String description) {
142         super.updateStatus(status, statusDetail, description);
143     }
144
145     @Override
146     protected Configuration getConfig() {
147         return super.getConfig();
148     }
149
150     private Runnable pollingRunnable = () -> {
151         try {
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());
156             } else {
157                 if (getThing().getStatus() == ThingStatus.ONLINE) {
158                     ByteBuffer response = cache.get(CACHE_REPORT_1);
159                     if (response != null) {
160                         onData(response);
161                     }
162
163                     Thread.sleep(REPORT_INTERVAL);
164
165                     response = cache.get(CACHE_REPORT_2);
166                     if (response != null) {
167                         onData(response);
168                     }
169
170                     Thread.sleep(REPORT_INTERVAL);
171
172                     response = cache.get(CACHE_REPORT_3);
173                     if (response != null) {
174                         onData(response);
175                     }
176
177                     if (isReport100needed) {
178                         Thread.sleep(REPORT_INTERVAL);
179
180                         response = cache.get(CACHE_REPORT_100);
181                         if (response != null) {
182                             onData(response);
183                         }
184                         isReport100needed = false;
185                     }
186                 }
187             }
188         } catch (NumberFormatException | IOException e) {
189             logger.debug("An exception occurred while polling the KEBA KeContact '{}': {}", getThing().getUID(),
190                     e.getMessage(), e);
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());
196         }
197     };
198
199     protected void onData(ByteBuffer byteBuffer) {
200         String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
201         response = StringUtils.chomp(response);
202
203         if (response.contains("TCH-OK")) {
204             // ignore confirmation messages which are not JSON
205             return;
206         }
207
208         try {
209             JsonObject readObject = parser.parse(response).getAsJsonObject();
210
211             for (Entry<String, JsonElement> entry : readObject.entrySet()) {
212                 switch (entry.getKey()) {
213                     case "Product": {
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")) {
219                             type = KebaType.P20;
220                         } else if (product.contains("P30")) {
221                             type = KebaType.P30;
222                         }
223                         series = KebaSeries.getSeries(product.substring(13, 14).charAt(0));
224                         break;
225                     }
226                     case "Serial": {
227                         Map<String, String> properties = editProperties();
228                         properties.put(CHANNEL_SERIAL, entry.getValue().getAsString());
229                         updateProperties(properties);
230                         break;
231                     }
232                     case "Firmware": {
233                         Map<String, String> properties = editProperties();
234                         properties.put(CHANNEL_FIRMWARE, entry.getValue().getAsString());
235                         updateProperties(properties);
236                         break;
237                     }
238                     case "Plug": {
239                         int state = entry.getValue().getAsInt();
240                         switch (state) {
241                             case 0: {
242                                 updateState(CHANNEL_WALLBOX, OnOffType.OFF);
243                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
244                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
245                                 break;
246                             }
247                             case 1: {
248                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
249                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
250                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
251                                 break;
252                             }
253                             case 3: {
254                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
255                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
256                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
257                                 break;
258                             }
259                             case 5: {
260                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
261                                 updateState(CHANNEL_VEHICLE, OnOffType.ON);
262                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
263                                 break;
264                             }
265                             case 7: {
266                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
267                                 updateState(CHANNEL_VEHICLE, OnOffType.ON);
268                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
269                                 break;
270                             }
271                         }
272                         break;
273                     }
274                     case "State": {
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;
281                             lastState = state;
282                         }
283                         break;
284                     }
285                     case "Enable sys": {
286                         int state = entry.getValue().getAsInt();
287                         switch (state) {
288                             case 1: {
289                                 updateState(CHANNEL_ENABLED, OnOffType.ON);
290                                 break;
291                             }
292                             default: {
293                                 updateState(CHANNEL_ENABLED, OnOffType.OFF);
294                                 break;
295                             }
296                         }
297                         break;
298                     }
299                     case "Curr HW": {
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)));
310                             }
311                         } else {
312                             logger.debug("maxSystemCurrent is 0. Ignoring.");
313                         }
314                         break;
315                     }
316                     case "Curr user": {
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))));
323                         }
324                         break;
325                     }
326                     case "Curr FS": {
327                         int state = entry.getValue().getAsInt();
328                         State newState = new DecimalType(state);
329                         updateState(CHANNEL_FAILSAFE_CURRENT, newState);
330                         break;
331                     }
332                     case "Max curr": {
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));
337                         break;
338                     }
339                     case "Output": {
340                         int state = entry.getValue().getAsInt();
341                         switch (state) {
342                             case 1: {
343                                 updateState(CHANNEL_OUTPUT, OnOffType.ON);
344                                 break;
345                             }
346                             default: {
347                                 updateState(CHANNEL_OUTPUT, OnOffType.OFF);
348                                 break;
349                             }
350                         }
351                         break;
352                     }
353                     case "Input": {
354                         int state = entry.getValue().getAsInt();
355                         switch (state) {
356                             case 1: {
357                                 updateState(CHANNEL_INPUT, OnOffType.ON);
358                                 break;
359                             }
360                             default: {
361                                 updateState(CHANNEL_INPUT, OnOffType.OFF);
362                                 break;
363                             }
364                         }
365                         break;
366                     }
367                     case "Sec": {
368                         long state = entry.getValue().getAsLong();
369
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"));
375
376                         updateState(CHANNEL_UPTIME, new DateTimeType(pFormatter.format(uptime.getTime())));
377                         break;
378                     }
379                     case "U1": {
380                         int state = entry.getValue().getAsInt();
381                         State newState = new DecimalType(state);
382                         updateState(CHANNEL_U1, newState);
383                         break;
384                     }
385                     case "U2": {
386                         int state = entry.getValue().getAsInt();
387                         State newState = new DecimalType(state);
388                         updateState(CHANNEL_U2, newState);
389                         break;
390                     }
391                     case "U3": {
392                         int state = entry.getValue().getAsInt();
393                         State newState = new DecimalType(state);
394                         updateState(CHANNEL_U3, newState);
395                         break;
396                     }
397                     case "I1": {
398                         int state = entry.getValue().getAsInt();
399                         State newState = new DecimalType(state);
400                         updateState(CHANNEL_I1, newState);
401                         break;
402                     }
403                     case "I2": {
404                         int state = entry.getValue().getAsInt();
405                         State newState = new DecimalType(state);
406                         updateState(CHANNEL_I2, newState);
407                         break;
408                     }
409                     case "I3": {
410                         int state = entry.getValue().getAsInt();
411                         State newState = new DecimalType(state);
412                         updateState(CHANNEL_I3, newState);
413                         break;
414                     }
415                     case "P": {
416                         long state = entry.getValue().getAsLong();
417                         State newState = new DecimalType(state / 1000);
418                         updateState(CHANNEL_POWER, newState);
419                         break;
420                     }
421                     case "PF": {
422                         int state = entry.getValue().getAsInt();
423                         State newState = new PercentType(state / 10);
424                         updateState(CHANNEL_POWER_FACTOR, newState);
425                         break;
426                     }
427                     case "E pres": {
428                         long state = entry.getValue().getAsLong();
429                         State newState = new DecimalType(state / 10);
430                         updateState(CHANNEL_SESSION_CONSUMPTION, newState);
431                         break;
432                     }
433                     case "E total": {
434                         long state = entry.getValue().getAsLong();
435                         State newState = new DecimalType(state / 10);
436                         updateState(CHANNEL_TOTAL_CONSUMPTION, newState);
437                         break;
438                     }
439                     case "AuthON": {
440                         int state = entry.getValue().getAsInt();
441                         State newState = new DecimalType(state);
442                         updateState(CHANNEL_AUTHON, newState);
443                         break;
444                     }
445                     case "Authreq": {
446                         int state = entry.getValue().getAsInt();
447                         State newState = new DecimalType(state);
448                         updateState(CHANNEL_AUTHREQ, newState);
449                         break;
450                     }
451                     case "RFID tag": {
452                         String state = entry.getValue().getAsString().trim();
453                         State newState = new StringType(state);
454                         updateState(CHANNEL_SESSION_RFID_TAG, newState);
455                         break;
456                     }
457                     case "RFID class": {
458                         String state = entry.getValue().getAsString().trim();
459                         State newState = new StringType(state);
460                         updateState(CHANNEL_SESSION_RFID_CLASS, newState);
461                         break;
462                     }
463                     case "Session ID": {
464                         int state = entry.getValue().getAsInt();
465                         State newState = new DecimalType(state);
466                         updateState(CHANNEL_SESSION_SESSION_ID, newState);
467                         break;
468                     }
469                     case "Setenergy": {
470                         int state = entry.getValue().getAsInt() / 10;
471                         State newState = new DecimalType(state);
472                         updateState(CHANNEL_SETENERGY, newState);
473                         break;
474                     }
475                 }
476             }
477         } catch (JsonParseException e) {
478             logger.debug("Invalid JSON data will be ignored: '{}'", response);
479         }
480     }
481
482     @Override
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
487         } else {
488             switch (channelUID.getId()) {
489                 case CHANNEL_MAX_PRESET_CURRENT: {
490                     if (command instanceof DecimalType) {
491                         transceiver.send(
492                                 "curr " + String.valueOf(
493                                         Math.min(Math.max(6000, ((DecimalType) command).intValue()), maxSystemCurrent)),
494                                 this);
495                     }
496                     break;
497                 }
498                 case CHANNEL_MAX_PRESET_CURRENT_RANGE: {
499                     if (command instanceof OnOffType || command instanceof IncreaseDecreaseType
500                             || command instanceof PercentType) {
501                         int newValue = 6000;
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) {
509                             newValue = 6000;
510                         } else if (command instanceof PercentType) {
511                             newValue = 6000 + (maxSystemCurrent - 6000) * ((PercentType) command).intValue() / 100;
512                         } else {
513                             return;
514                         }
515
516                         transceiver.send("curr " + String.valueOf(newValue), this);
517                     }
518                     break;
519                 }
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);
526                         } else {
527                             return;
528                         }
529                     }
530                     break;
531                 }
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);
538                         } else {
539                             return;
540                         }
541                     }
542                     break;
543                 }
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);
550                         } else {
551                             logger.warn("'Display' is not supported on a KEBA KeContact {}:{}", type, series);
552                         }
553                     }
554                     break;
555                 }
556                 case CHANNEL_SETENERGY: {
557                     if (command instanceof DecimalType) {
558                         transceiver.send(
559                                 "setenergy " + String.valueOf(
560                                         Math.min(Math.max(0, ((DecimalType) command).intValue() * 10), 999999999)),
561                                 this);
562                     }
563                     break;
564                 }
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
569                         // station)
570                         transceiver.send("start " + cmd, this);
571                     }
572                     break;
573                 }
574             }
575         }
576     }
577 }