]> git.basschouten.com Git - openhab-addons.git/blob
29b93acc3431b33588e412175e5f910380e4b368
[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     protected final JsonParser parser = new JsonParser();
86     private final KeContactTransceiver transceiver;
87
88     private ScheduledFuture<?> pollingJob;
89     private ExpiringCacheMap<String, ByteBuffer> cache;
90
91     private int maxPresetCurrent = 0;
92     private int maxSystemCurrent = 63000;
93     private KebaType type;
94     private KebaSeries series;
95     private int lastState = -1; // trigger a report100 at startup
96     private boolean isReport100needed = true;
97
98     public KeContactHandler(Thing thing, KeContactTransceiver transceiver) {
99         super(thing);
100         this.transceiver = transceiver;
101     }
102
103     @Override
104     public void initialize() {
105         try {
106             if (isKebaReachable()) {
107                 transceiver.registerHandler(this);
108
109                 int refreshInterval = getRefreshInterval();
110                 cache = new ExpiringCacheMap<>(Math.max(refreshInterval - 5, 0) * 1000);
111
112                 cache.put(CACHE_REPORT_1, () -> transceiver.send("report 1", getHandler()));
113                 cache.put(CACHE_REPORT_2, () -> transceiver.send("report 2", getHandler()));
114                 cache.put(CACHE_REPORT_3, () -> transceiver.send("report 3", getHandler()));
115                 cache.put(CACHE_REPORT_100, () -> transceiver.send("report 100", getHandler()));
116
117                 if (pollingJob == null || pollingJob.isCancelled()) {
118                     pollingJob = scheduler.scheduleWithFixedDelay(this::pollingRunnable, 0, refreshInterval,
119                             TimeUnit.SECONDS);
120                 }
121             } else {
122                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
123                         "IP address or port number not set");
124             }
125         } catch (IOException e) {
126             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
127                     "Exception during initialization of binding: " + e.toString());
128         }
129     }
130
131     private boolean isKebaReachable() throws IOException {
132         boolean isReachable = false;
133         SocketAddress sockAddr = new InetSocketAddress(getIPAddress(), SOCKET_CHECK_PORT_NUMBER);
134         Socket socket = new Socket();
135         try {
136             socket.connect(sockAddr, SOCKET_TIME_OUT_MS);
137             isReachable = true;
138         } finally {
139             socket.close();
140         }
141         logger.debug("isKebaReachable() returns {}", isReachable);
142         return isReachable;
143     }
144
145     @Override
146     public void dispose() {
147         if (pollingJob != null && !pollingJob.isCancelled()) {
148             pollingJob.cancel(true);
149             pollingJob = null;
150         }
151
152         transceiver.unRegisterHandler(this);
153     }
154
155     public String getIPAddress() {
156         return getConfig().get(IP_ADDRESS) != null ? (String) getConfig().get(IP_ADDRESS) : "";
157     }
158
159     public int getRefreshInterval() {
160         return getConfig().get(POLLING_REFRESH_INTERVAL) != null
161                 ? ((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue()
162                 : POLLING_REFRESH_INTERVAL_DEFAULT;
163     }
164
165     private KeContactHandler getHandler() {
166         return this;
167     }
168
169     @Override
170     public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, String description) {
171         super.updateStatus(status, statusDetail, description);
172     }
173
174     @Override
175     protected Configuration getConfig() {
176         return super.getConfig();
177     }
178
179     private void pollingRunnable() {
180         try {
181             logger.debug("Running pollingRunnable to connect Keba wallbox");
182             long stamp = System.currentTimeMillis();
183             if (!isKebaReachable()) {
184                 logger.debug("isKebaReachable() timed out after '{}' milliseconds", System.currentTimeMillis() - stamp);
185                 transceiver.unRegisterHandler(getHandler());
186             } else {
187                 if (getThing().getStatus() == ThingStatus.ONLINE) {
188                     ByteBuffer response = cache.get(CACHE_REPORT_1);
189                     if (response != null) {
190                         onData(response);
191                     }
192
193                     Thread.sleep(REPORT_INTERVAL);
194
195                     response = cache.get(CACHE_REPORT_2);
196                     if (response != null) {
197                         onData(response);
198                     }
199
200                     Thread.sleep(REPORT_INTERVAL);
201
202                     response = cache.get(CACHE_REPORT_3);
203                     if (response != null) {
204                         onData(response);
205                     }
206
207                     if (isReport100needed) {
208                         Thread.sleep(REPORT_INTERVAL);
209
210                         response = cache.get(CACHE_REPORT_100);
211                         if (response != null) {
212                             onData(response);
213                         }
214                         isReport100needed = false;
215                     }
216                 }
217             }
218         } catch (NumberFormatException | IOException e) {
219             logger.debug("An exception occurred while polling the KEBA KeContact '{}': {}", getThing().getUID(),
220                     e.getMessage(), e);
221             Thread.currentThread().interrupt();
222             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
223                     "An exception occurred while while polling the charging station");
224         } catch (InterruptedException e) {
225             logger.debug("Polling job has been interrupted for handler of thing '{}'.", getThing().getUID());
226         }
227     }
228
229     protected void onData(ByteBuffer byteBuffer) {
230         String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
231         response = StringUtils.chomp(response);
232
233         if (response.contains("TCH-OK")) {
234             // ignore confirmation messages which are not JSON
235             return;
236         }
237
238         try {
239             JsonObject readObject = parser.parse(response).getAsJsonObject();
240
241             for (Entry<String, JsonElement> entry : readObject.entrySet()) {
242                 switch (entry.getKey()) {
243                     case "Product": {
244                         Map<String, String> properties = editProperties();
245                         String product = entry.getValue().getAsString().trim();
246                         properties.put(CHANNEL_MODEL, product);
247                         updateProperties(properties);
248                         if (product.contains("P20")) {
249                             type = KebaType.P20;
250                         } else if (product.contains("P30")) {
251                             type = KebaType.P30;
252                         }
253                         series = KebaSeries.getSeries(product.substring(13, 14).charAt(0));
254                         break;
255                     }
256                     case "Serial": {
257                         Map<String, String> properties = editProperties();
258                         properties.put(CHANNEL_SERIAL, entry.getValue().getAsString());
259                         updateProperties(properties);
260                         break;
261                     }
262                     case "Firmware": {
263                         Map<String, String> properties = editProperties();
264                         properties.put(CHANNEL_FIRMWARE, entry.getValue().getAsString());
265                         updateProperties(properties);
266                         break;
267                     }
268                     case "Plug": {
269                         int state = entry.getValue().getAsInt();
270                         switch (state) {
271                             case 0: {
272                                 updateState(CHANNEL_WALLBOX, OnOffType.OFF);
273                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
274                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
275                                 break;
276                             }
277                             case 1: {
278                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
279                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
280                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
281                                 break;
282                             }
283                             case 3: {
284                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
285                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
286                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
287                                 break;
288                             }
289                             case 5: {
290                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
291                                 updateState(CHANNEL_VEHICLE, OnOffType.ON);
292                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
293                                 break;
294                             }
295                             case 7: {
296                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
297                                 updateState(CHANNEL_VEHICLE, OnOffType.ON);
298                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
299                                 break;
300                             }
301                         }
302                         break;
303                     }
304                     case "State": {
305                         int state = entry.getValue().getAsInt();
306                         State newState = new DecimalType(state);
307                         updateState(CHANNEL_STATE, newState);
308                         if (lastState != state) {
309                             // the state is different from the last one, so we will trigger a report100
310                             isReport100needed = true;
311                             lastState = state;
312                         }
313                         break;
314                     }
315                     case "Enable sys": {
316                         int state = entry.getValue().getAsInt();
317                         switch (state) {
318                             case 1: {
319                                 updateState(CHANNEL_ENABLED, OnOffType.ON);
320                                 break;
321                             }
322                             default: {
323                                 updateState(CHANNEL_ENABLED, OnOffType.OFF);
324                                 break;
325                             }
326                         }
327                         break;
328                     }
329                     case "Curr HW": {
330                         int state = entry.getValue().getAsInt();
331                         maxSystemCurrent = state;
332                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
333                         updateState(CHANNEL_MAX_SYSTEM_CURRENT, newState);
334                         if (maxSystemCurrent != 0) {
335                             if (maxSystemCurrent < maxPresetCurrent) {
336                                 transceiver.send("curr " + String.valueOf(maxSystemCurrent), this);
337                                 updateState(CHANNEL_MAX_PRESET_CURRENT,
338                                         new QuantityType<ElectricCurrent>(maxSystemCurrent / 1000.0, Units.AMPERE));
339                                 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
340                                         (maxSystemCurrent - 6000) * 100 / (maxSystemCurrent - 6000), Units.PERCENT));
341                             }
342                         } else {
343                             logger.debug("maxSystemCurrent is 0. Ignoring.");
344                         }
345                         break;
346                     }
347                     case "Curr user": {
348                         int state = entry.getValue().getAsInt();
349                         maxPresetCurrent = state;
350                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
351                         updateState(CHANNEL_MAX_PRESET_CURRENT, newState);
352                         if (maxSystemCurrent != 0) {
353                             updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
354                                     Math.min(100, (state - 6000) * 100 / (maxSystemCurrent - 6000)), Units.PERCENT));
355                         }
356                         break;
357                     }
358                     case "Curr FS": {
359                         int state = entry.getValue().getAsInt();
360                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
361                         updateState(CHANNEL_FAILSAFE_CURRENT, newState);
362                         break;
363                     }
364                     case "Max curr": {
365                         int state = entry.getValue().getAsInt();
366                         maxPresetCurrent = state;
367                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
368                         updateState(CHANNEL_PILOT_CURRENT, newState);
369                         break;
370                     }
371                     case "Max curr %": {
372                         int state = entry.getValue().getAsInt();
373                         State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
374                         updateState(CHANNEL_PILOT_PWM, newState);
375                         break;
376                     }
377                     case "Output": {
378                         int state = entry.getValue().getAsInt();
379                         switch (state) {
380                             case 1: {
381                                 updateState(CHANNEL_OUTPUT, OnOffType.ON);
382                                 break;
383                             }
384                             default: {
385                                 updateState(CHANNEL_OUTPUT, OnOffType.OFF);
386                                 break;
387                             }
388                         }
389                         break;
390                     }
391                     case "Input": {
392                         int state = entry.getValue().getAsInt();
393                         switch (state) {
394                             case 1: {
395                                 updateState(CHANNEL_INPUT, OnOffType.ON);
396                                 break;
397                             }
398                             default: {
399                                 updateState(CHANNEL_INPUT, OnOffType.OFF);
400                                 break;
401                             }
402                         }
403                         break;
404                     }
405                     case "Sec": {
406                         long state = entry.getValue().getAsLong();
407                         State newState = new QuantityType<Time>(state, Units.SECOND);
408                         updateState(CHANNEL_UPTIME, newState);
409                         break;
410                     }
411                     case "U1": {
412                         int state = entry.getValue().getAsInt();
413                         State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
414                         updateState(CHANNEL_U1, newState);
415                         break;
416                     }
417                     case "U2": {
418                         int state = entry.getValue().getAsInt();
419                         State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
420                         updateState(CHANNEL_U2, newState);
421                         break;
422                     }
423                     case "U3": {
424                         int state = entry.getValue().getAsInt();
425                         State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
426                         updateState(CHANNEL_U3, newState);
427                         break;
428                     }
429                     case "I1": {
430                         int state = entry.getValue().getAsInt();
431                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
432                         updateState(CHANNEL_I1, newState);
433                         break;
434                     }
435                     case "I2": {
436                         int state = entry.getValue().getAsInt();
437                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
438                         updateState(CHANNEL_I2, newState);
439                         break;
440                     }
441                     case "I3": {
442                         int state = entry.getValue().getAsInt();
443                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
444                         updateState(CHANNEL_I3, newState);
445                         break;
446                     }
447                     case "P": {
448                         long state = entry.getValue().getAsLong();
449                         State newState = new QuantityType<Power>(state / 1000.0, Units.WATT);
450                         updateState(CHANNEL_POWER, newState);
451                         break;
452                     }
453                     case "PF": {
454                         int state = entry.getValue().getAsInt();
455                         State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
456                         updateState(CHANNEL_POWER_FACTOR, newState);
457                         break;
458                     }
459                     case "E pres": {
460                         long state = entry.getValue().getAsLong();
461                         State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
462                         updateState(CHANNEL_SESSION_CONSUMPTION, newState);
463                         break;
464                     }
465                     case "E total": {
466                         long state = entry.getValue().getAsLong();
467                         State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
468                         updateState(CHANNEL_TOTAL_CONSUMPTION, newState);
469                         break;
470                     }
471                     case "AuthON": {
472                         int state = entry.getValue().getAsInt();
473                         State newState = new DecimalType(state);
474                         updateState(CHANNEL_AUTHON, newState);
475                         break;
476                     }
477                     case "Authreq": {
478                         int state = entry.getValue().getAsInt();
479                         State newState = new DecimalType(state);
480                         updateState(CHANNEL_AUTHREQ, newState);
481                         break;
482                     }
483                     case "RFID tag": {
484                         String state = entry.getValue().getAsString().trim();
485                         State newState = new StringType(state);
486                         updateState(CHANNEL_SESSION_RFID_TAG, newState);
487                         break;
488                     }
489                     case "RFID class": {
490                         String state = entry.getValue().getAsString().trim();
491                         State newState = new StringType(state);
492                         updateState(CHANNEL_SESSION_RFID_CLASS, newState);
493                         break;
494                     }
495                     case "Session ID": {
496                         int state = entry.getValue().getAsInt();
497                         State newState = new DecimalType(state);
498                         updateState(CHANNEL_SESSION_SESSION_ID, newState);
499                         break;
500                     }
501                     case "Setenergy": {
502                         int state = entry.getValue().getAsInt();
503                         State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
504                         updateState(CHANNEL_SETENERGY, newState);
505                         break;
506                     }
507                 }
508             }
509         } catch (JsonParseException e) {
510             logger.debug("Invalid JSON data will be ignored: '{}'", response);
511         }
512     }
513
514     @Override
515     public void handleCommand(ChannelUID channelUID, Command command) {
516         if ((command instanceof RefreshType)) {
517             // let's assume we do frequent enough polling and ignore the REFRESH request here
518             // in order to prevent too many channel state updates
519         } else {
520             switch (channelUID.getId()) {
521                 case CHANNEL_MAX_PRESET_CURRENT: {
522                     if (command instanceof QuantityType<?>) {
523                         QuantityType<?> value = ((QuantityType<?>) command).toUnit("mA");
524
525                         transceiver.send(
526                                 "curr " + String.valueOf(Math.min(Math.max(6000, value.intValue()), maxSystemCurrent)),
527                                 this);
528                     }
529                     break;
530                 }
531                 case CHANNEL_MAX_PRESET_CURRENT_RANGE: {
532                     if (command instanceof OnOffType || command instanceof IncreaseDecreaseType
533                             || command instanceof QuantityType<?>) {
534                         long newValue = 6000;
535                         if (command == IncreaseDecreaseType.INCREASE) {
536                             newValue = Math.min(Math.max(6000, maxPresetCurrent + 1), maxSystemCurrent);
537                         } else if (command == IncreaseDecreaseType.DECREASE) {
538                             newValue = Math.min(Math.max(6000, maxPresetCurrent - 1), maxSystemCurrent);
539                         } else if (command == OnOffType.ON) {
540                             newValue = maxSystemCurrent;
541                         } else if (command == OnOffType.OFF) {
542                             newValue = 6000;
543                         } else if (command instanceof QuantityType<?>) {
544                             QuantityType<?> value = ((QuantityType<?>) command).toUnit("%");
545                             newValue = Math.round(6000 + (maxSystemCurrent - 6000) * value.doubleValue() / 100.0);
546                         } else {
547                             return;
548                         }
549                         transceiver.send("curr " + String.valueOf(newValue), this);
550                     }
551                     break;
552                 }
553                 case CHANNEL_ENABLED: {
554                     if (command instanceof OnOffType) {
555                         if (command == OnOffType.ON) {
556                             transceiver.send("ena 1", this);
557                         } else if (command == OnOffType.OFF) {
558                             transceiver.send("ena 0", this);
559                         } else {
560                             return;
561                         }
562                     }
563                     break;
564                 }
565                 case CHANNEL_OUTPUT: {
566                     if (command instanceof OnOffType) {
567                         if (command == OnOffType.ON) {
568                             transceiver.send("output 1", this);
569                         } else if (command == OnOffType.OFF) {
570                             transceiver.send("output 0", this);
571                         } else {
572                             return;
573                         }
574                     }
575                     break;
576                 }
577                 case CHANNEL_DISPLAY: {
578                     if (command instanceof StringType) {
579                         if (type == KebaType.P30 && (series == KebaSeries.C || series == KebaSeries.X)) {
580                             String cmd = command.toString();
581                             int maxLength = (cmd.length() < 23) ? cmd.length() : 23;
582                             transceiver.send("display 0 0 0 0 " + cmd.substring(0, maxLength), this);
583                         } else {
584                             logger.warn("'Display' is not supported on a KEBA KeContact {}:{}", type, series);
585                         }
586                     }
587                     break;
588                 }
589                 case CHANNEL_SETENERGY: {
590                     if (command instanceof QuantityType<?>) {
591                         QuantityType<?> value = ((QuantityType<?>) command).toUnit(Units.WATT_HOUR);
592                         transceiver.send(
593                                 "setenergy " + String.valueOf(
594                                         Math.min(Math.max(0, Math.round(value.doubleValue() * 10.0)), 999999999)),
595                                 this);
596                     }
597                     break;
598                 }
599                 case CHANNEL_AUTHENTICATE: {
600                     if (command instanceof StringType) {
601                         String cmd = command.toString();
602                         // cmd must contain ID + CLASS (works only if the RFID TAG is in the whitelist of the Keba
603                         // station)
604                         transceiver.send("start " + cmd, this);
605                     }
606                     break;
607                 }
608             }
609         }
610     }
611 }