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