]> git.basschouten.com Git - openhab-addons.git/blob
af2b999a91516932bc7ea88c339a387589e14db0
[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.InetSocketAddress;
20 import java.net.Socket;
21 import java.net.SocketAddress;
22 import java.nio.ByteBuffer;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
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;
34
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;
56
57 import com.google.gson.JsonElement;
58 import com.google.gson.JsonObject;
59 import com.google.gson.JsonParseException;
60 import com.google.gson.JsonParser;
61
62 /**
63  * The {@link KeContactHandler} is responsible for handling commands, which
64  * are sent to one of the channels.
65  *
66  * @author Karel Goderis - Initial contribution
67  */
68 public class KeContactHandler extends BaseThingHandler {
69
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;
82
83     private final Logger logger = LoggerFactory.getLogger(KeContactHandler.class);
84
85     private final KeContactTransceiver transceiver;
86
87     private ScheduledFuture<?> pollingJob;
88     private ExpiringCacheMap<String, ByteBuffer> cache;
89
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;
96
97     public KeContactHandler(Thing thing, KeContactTransceiver transceiver) {
98         super(thing);
99         this.transceiver = transceiver;
100     }
101
102     @Override
103     public void initialize() {
104         try {
105             if (isKebaReachable()) {
106                 transceiver.registerHandler(this);
107
108                 int refreshInterval = getRefreshInterval();
109                 cache = new ExpiringCacheMap<>(Math.max(refreshInterval - 5, 0) * 1000);
110
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()));
115
116                 if (pollingJob == null || pollingJob.isCancelled()) {
117                     pollingJob = scheduler.scheduleWithFixedDelay(this::pollingRunnable, 0, refreshInterval,
118                             TimeUnit.SECONDS);
119                 }
120             } else {
121                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
122                         "IP address or port number not set");
123             }
124         } catch (IOException e) {
125             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
126                     "Exception during initialization of binding: " + e.toString());
127         }
128     }
129
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();
134         try {
135             socket.connect(sockAddr, SOCKET_TIME_OUT_MS);
136             isReachable = true;
137         } finally {
138             socket.close();
139         }
140         logger.debug("isKebaReachable() returns {}", isReachable);
141         return isReachable;
142     }
143
144     @Override
145     public void dispose() {
146         if (pollingJob != null && !pollingJob.isCancelled()) {
147             pollingJob.cancel(true);
148             pollingJob = null;
149         }
150
151         transceiver.unRegisterHandler(this);
152     }
153
154     public String getIPAddress() {
155         return getConfig().get(IP_ADDRESS) != null ? (String) getConfig().get(IP_ADDRESS) : "";
156     }
157
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;
162     }
163
164     private KeContactHandler getHandler() {
165         return this;
166     }
167
168     @Override
169     public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, String description) {
170         super.updateStatus(status, statusDetail, description);
171     }
172
173     @Override
174     protected Configuration getConfig() {
175         return super.getConfig();
176     }
177
178     private void pollingRunnable() {
179         try {
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());
185             } else {
186                 if (getThing().getStatus() == ThingStatus.ONLINE) {
187                     ByteBuffer response = cache.get(CACHE_REPORT_1);
188                     if (response != null) {
189                         onData(response);
190                     }
191
192                     Thread.sleep(REPORT_INTERVAL);
193
194                     response = cache.get(CACHE_REPORT_2);
195                     if (response != null) {
196                         onData(response);
197                     }
198
199                     Thread.sleep(REPORT_INTERVAL);
200
201                     response = cache.get(CACHE_REPORT_3);
202                     if (response != null) {
203                         onData(response);
204                     }
205
206                     if (isReport100needed) {
207                         Thread.sleep(REPORT_INTERVAL);
208
209                         response = cache.get(CACHE_REPORT_100);
210                         if (response != null) {
211                             onData(response);
212                         }
213                         isReport100needed = false;
214                     }
215                 }
216             }
217         } catch (NumberFormatException | IOException e) {
218             logger.debug("An exception occurred while polling the KEBA KeContact '{}': {}", getThing().getUID(),
219                     e.getMessage(), e);
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());
225         }
226     }
227
228     protected void onData(ByteBuffer byteBuffer) {
229         String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
230         response = StringUtils.chomp(response);
231
232         if (response.contains("TCH-OK")) {
233             // ignore confirmation messages which are not JSON
234             return;
235         }
236
237         try {
238             JsonObject readObject = JsonParser.parseString(response).getAsJsonObject();
239
240             for (Entry<String, JsonElement> entry : readObject.entrySet()) {
241                 switch (entry.getKey()) {
242                     case "Product": {
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")) {
248                             type = KebaType.P20;
249                         } else if (product.contains("P30")) {
250                             type = KebaType.P30;
251                         }
252                         series = KebaSeries.getSeries(product.substring(13, 14).charAt(0));
253                         break;
254                     }
255                     case "Serial": {
256                         Map<String, String> properties = editProperties();
257                         properties.put(CHANNEL_SERIAL, entry.getValue().getAsString());
258                         updateProperties(properties);
259                         break;
260                     }
261                     case "Firmware": {
262                         Map<String, String> properties = editProperties();
263                         properties.put(CHANNEL_FIRMWARE, entry.getValue().getAsString());
264                         updateProperties(properties);
265                         break;
266                     }
267                     case "Plug": {
268                         int state = entry.getValue().getAsInt();
269                         switch (state) {
270                             case 0: {
271                                 updateState(CHANNEL_WALLBOX, OnOffType.OFF);
272                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
273                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
274                                 break;
275                             }
276                             case 1: {
277                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
278                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
279                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
280                                 break;
281                             }
282                             case 3: {
283                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
284                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
285                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
286                                 break;
287                             }
288                             case 5: {
289                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
290                                 updateState(CHANNEL_VEHICLE, OnOffType.ON);
291                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
292                                 break;
293                             }
294                             case 7: {
295                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
296                                 updateState(CHANNEL_VEHICLE, OnOffType.ON);
297                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
298                                 break;
299                             }
300                         }
301                         break;
302                     }
303                     case "State": {
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;
310                             lastState = state;
311                         }
312                         break;
313                     }
314                     case "Enable sys": {
315                         int state = entry.getValue().getAsInt();
316                         switch (state) {
317                             case 1: {
318                                 updateState(CHANNEL_ENABLED, OnOffType.ON);
319                                 break;
320                             }
321                             default: {
322                                 updateState(CHANNEL_ENABLED, OnOffType.OFF);
323                                 break;
324                             }
325                         }
326                         break;
327                     }
328                     case "Curr HW": {
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));
340                             }
341                         } else {
342                             logger.debug("maxSystemCurrent is 0. Ignoring.");
343                         }
344                         break;
345                     }
346                     case "Curr user": {
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));
354                         }
355                         break;
356                     }
357                     case "Curr FS": {
358                         int state = entry.getValue().getAsInt();
359                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
360                         updateState(CHANNEL_FAILSAFE_CURRENT, newState);
361                         break;
362                     }
363                     case "Max curr": {
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);
368                         break;
369                     }
370                     case "Max curr %": {
371                         int state = entry.getValue().getAsInt();
372                         State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
373                         updateState(CHANNEL_PILOT_PWM, newState);
374                         break;
375                     }
376                     case "Output": {
377                         int state = entry.getValue().getAsInt();
378                         switch (state) {
379                             case 1: {
380                                 updateState(CHANNEL_OUTPUT, OnOffType.ON);
381                                 break;
382                             }
383                             default: {
384                                 updateState(CHANNEL_OUTPUT, OnOffType.OFF);
385                                 break;
386                             }
387                         }
388                         break;
389                     }
390                     case "Input": {
391                         int state = entry.getValue().getAsInt();
392                         switch (state) {
393                             case 1: {
394                                 updateState(CHANNEL_INPUT, OnOffType.ON);
395                                 break;
396                             }
397                             default: {
398                                 updateState(CHANNEL_INPUT, OnOffType.OFF);
399                                 break;
400                             }
401                         }
402                         break;
403                     }
404                     case "Sec": {
405                         long state = entry.getValue().getAsLong();
406                         State newState = new QuantityType<Time>(state, Units.SECOND);
407                         updateState(CHANNEL_UPTIME, newState);
408                         break;
409                     }
410                     case "U1": {
411                         int state = entry.getValue().getAsInt();
412                         State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
413                         updateState(CHANNEL_U1, newState);
414                         break;
415                     }
416                     case "U2": {
417                         int state = entry.getValue().getAsInt();
418                         State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
419                         updateState(CHANNEL_U2, newState);
420                         break;
421                     }
422                     case "U3": {
423                         int state = entry.getValue().getAsInt();
424                         State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
425                         updateState(CHANNEL_U3, newState);
426                         break;
427                     }
428                     case "I1": {
429                         int state = entry.getValue().getAsInt();
430                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
431                         updateState(CHANNEL_I1, newState);
432                         break;
433                     }
434                     case "I2": {
435                         int state = entry.getValue().getAsInt();
436                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
437                         updateState(CHANNEL_I2, newState);
438                         break;
439                     }
440                     case "I3": {
441                         int state = entry.getValue().getAsInt();
442                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
443                         updateState(CHANNEL_I3, newState);
444                         break;
445                     }
446                     case "P": {
447                         long state = entry.getValue().getAsLong();
448                         State newState = new QuantityType<Power>(state / 1000.0, Units.WATT);
449                         updateState(CHANNEL_POWER, newState);
450                         break;
451                     }
452                     case "PF": {
453                         int state = entry.getValue().getAsInt();
454                         State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
455                         updateState(CHANNEL_POWER_FACTOR, newState);
456                         break;
457                     }
458                     case "E pres": {
459                         long state = entry.getValue().getAsLong();
460                         State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
461                         updateState(CHANNEL_SESSION_CONSUMPTION, newState);
462                         break;
463                     }
464                     case "E total": {
465                         long state = entry.getValue().getAsLong();
466                         State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
467                         updateState(CHANNEL_TOTAL_CONSUMPTION, newState);
468                         break;
469                     }
470                     case "AuthON": {
471                         int state = entry.getValue().getAsInt();
472                         State newState = new DecimalType(state);
473                         updateState(CHANNEL_AUTHON, newState);
474                         break;
475                     }
476                     case "Authreq": {
477                         int state = entry.getValue().getAsInt();
478                         State newState = new DecimalType(state);
479                         updateState(CHANNEL_AUTHREQ, newState);
480                         break;
481                     }
482                     case "RFID tag": {
483                         String state = entry.getValue().getAsString().trim();
484                         State newState = new StringType(state);
485                         updateState(CHANNEL_SESSION_RFID_TAG, newState);
486                         break;
487                     }
488                     case "RFID class": {
489                         String state = entry.getValue().getAsString().trim();
490                         State newState = new StringType(state);
491                         updateState(CHANNEL_SESSION_RFID_CLASS, newState);
492                         break;
493                     }
494                     case "Session ID": {
495                         int state = entry.getValue().getAsInt();
496                         State newState = new DecimalType(state);
497                         updateState(CHANNEL_SESSION_SESSION_ID, newState);
498                         break;
499                     }
500                     case "Setenergy": {
501                         int state = entry.getValue().getAsInt();
502                         State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
503                         updateState(CHANNEL_SETENERGY, newState);
504                         break;
505                     }
506                 }
507             }
508         } catch (JsonParseException e) {
509             logger.debug("Invalid JSON data will be ignored: '{}'", response);
510         }
511     }
512
513     @Override
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
518         } else {
519             switch (channelUID.getId()) {
520                 case CHANNEL_MAX_PRESET_CURRENT: {
521                     if (command instanceof QuantityType<?>) {
522                         QuantityType<?> value = ((QuantityType<?>) command).toUnit("mA");
523
524                         transceiver.send(
525                                 "curr " + String.valueOf(Math.min(Math.max(6000, value.intValue()), maxSystemCurrent)),
526                                 this);
527                     }
528                     break;
529                 }
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) {
541                             newValue = 6000;
542                         } else if (command instanceof QuantityType<?>) {
543                             QuantityType<?> value = ((QuantityType<?>) command).toUnit("%");
544                             newValue = Math.round(6000 + (maxSystemCurrent - 6000) * value.doubleValue() / 100.0);
545                         } else {
546                             return;
547                         }
548                         transceiver.send("curr " + String.valueOf(newValue), this);
549                     }
550                     break;
551                 }
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);
558                         } else {
559                             return;
560                         }
561                     }
562                     break;
563                 }
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);
570                         } else {
571                             return;
572                         }
573                     }
574                     break;
575                 }
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);
582                         } else {
583                             logger.warn("'Display' is not supported on a KEBA KeContact {}:{}", type, series);
584                         }
585                     }
586                     break;
587                 }
588                 case CHANNEL_SETENERGY: {
589                     if (command instanceof QuantityType<?>) {
590                         QuantityType<?> value = ((QuantityType<?>) command).toUnit(Units.WATT_HOUR);
591                         transceiver.send(
592                                 "setenergy " + String.valueOf(
593                                         Math.min(Math.max(0, Math.round(value.doubleValue() * 10.0)), 999999999)),
594                                 this);
595                     }
596                     break;
597                 }
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
602                         // station)
603                         transceiver.send("start " + cmd, this);
604                     }
605                     break;
606                 }
607             }
608         }
609     }
610 }