]> git.basschouten.com Git - openhab-addons.git/blob
a16abfd30c82e3e8fee0b6e8743a8b3cfeaaf864
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
185                         "A timeout occurred while polling the charging station");
186             } else {
187                 ByteBuffer response = cache.get(CACHE_REPORT_1);
188                 if (response == null) {
189                     logger.debug("Missing response from Keba station for 'report 1'");
190                 } else {
191                     onData(response);
192                 }
193
194                 Thread.sleep(REPORT_INTERVAL);
195
196                 response = cache.get(CACHE_REPORT_2);
197                 if (response == null) {
198                     logger.debug("Missing response from Keba station for 'report 2'");
199                 } else {
200                     onData(response);
201                 }
202
203                 Thread.sleep(REPORT_INTERVAL);
204
205                 response = cache.get(CACHE_REPORT_3);
206                 if (response == null) {
207                     logger.debug("Missing response from Keba station for 'report 3'");
208                 } else {
209                     onData(response);
210                 }
211
212                 if (isReport100needed) {
213                     Thread.sleep(REPORT_INTERVAL);
214
215                     response = cache.get(CACHE_REPORT_100);
216                     if (response == null) {
217                         logger.debug("Missing response from Keba station for 'report 100'");
218                     } else {
219                         onData(response);
220                     }
221                     isReport100needed = false;
222                 }
223             }
224         } catch (IOException e) {
225             logger.debug("An error occurred while polling the KEBA KeContact '{}': {}", getThing().getUID(),
226                     e.getMessage(), e);
227             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
228                     "An error occurred while polling the charging station");
229         } catch (InterruptedException e) {
230             logger.debug("Polling job has been interrupted for handler of thing '{}'.", getThing().getUID());
231         }
232     }
233
234     protected void onData(ByteBuffer byteBuffer) {
235         if (getThing().getStatus() != ThingStatus.ONLINE) {
236             updateStatus(ThingStatus.ONLINE);
237         }
238
239         String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
240         response = StringUtils.chomp(response);
241
242         if (response.contains("TCH-OK")) {
243             // ignore confirmation messages which are not JSON
244             return;
245         }
246
247         try {
248             JsonObject readObject = JsonParser.parseString(response).getAsJsonObject();
249
250             for (Entry<String, JsonElement> entry : readObject.entrySet()) {
251                 switch (entry.getKey()) {
252                     case "Product": {
253                         Map<String, String> properties = editProperties();
254                         String product = entry.getValue().getAsString().trim();
255                         properties.put(CHANNEL_MODEL, product);
256                         updateProperties(properties);
257                         if (product.contains("P20")) {
258                             type = KebaType.P20;
259                         } else if (product.contains("P30")) {
260                             type = KebaType.P30;
261                         }
262                         series = KebaSeries.getSeries(product.substring(13, 14).charAt(0));
263                         break;
264                     }
265                     case "Serial": {
266                         Map<String, String> properties = editProperties();
267                         properties.put(CHANNEL_SERIAL, entry.getValue().getAsString());
268                         updateProperties(properties);
269                         break;
270                     }
271                     case "Firmware": {
272                         Map<String, String> properties = editProperties();
273                         properties.put(CHANNEL_FIRMWARE, entry.getValue().getAsString());
274                         updateProperties(properties);
275                         break;
276                     }
277                     case "Plug": {
278                         int state = entry.getValue().getAsInt();
279                         switch (state) {
280                             case 0: {
281                                 updateState(CHANNEL_WALLBOX, OnOffType.OFF);
282                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
283                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
284                                 break;
285                             }
286                             case 1: {
287                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
288                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
289                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
290                                 break;
291                             }
292                             case 3: {
293                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
294                                 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
295                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
296                                 break;
297                             }
298                             case 5: {
299                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
300                                 updateState(CHANNEL_VEHICLE, OnOffType.ON);
301                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
302                                 break;
303                             }
304                             case 7: {
305                                 updateState(CHANNEL_WALLBOX, OnOffType.ON);
306                                 updateState(CHANNEL_VEHICLE, OnOffType.ON);
307                                 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
308                                 break;
309                             }
310                         }
311                         break;
312                     }
313                     case "State": {
314                         int state = entry.getValue().getAsInt();
315                         State newState = new DecimalType(state);
316                         updateState(CHANNEL_STATE, newState);
317                         if (lastState != state) {
318                             // the state is different from the last one, so we will trigger a report100
319                             isReport100needed = true;
320                             lastState = state;
321                         }
322                         break;
323                     }
324                     case "Enable sys": {
325                         int state = entry.getValue().getAsInt();
326                         switch (state) {
327                             case 1: {
328                                 updateState(CHANNEL_ENABLED, OnOffType.ON);
329                                 break;
330                             }
331                             default: {
332                                 updateState(CHANNEL_ENABLED, OnOffType.OFF);
333                                 break;
334                             }
335                         }
336                         break;
337                     }
338                     case "Curr HW": {
339                         int state = entry.getValue().getAsInt();
340                         maxSystemCurrent = state;
341                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
342                         updateState(CHANNEL_MAX_SYSTEM_CURRENT, newState);
343                         if (maxSystemCurrent != 0) {
344                             if (maxSystemCurrent < maxPresetCurrent) {
345                                 transceiver.send("curr " + maxSystemCurrent, this);
346                                 updateState(CHANNEL_MAX_PRESET_CURRENT,
347                                         new QuantityType<ElectricCurrent>(maxSystemCurrent / 1000.0, Units.AMPERE));
348                                 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
349                                         (maxSystemCurrent - 6000) * 100 / (maxSystemCurrent - 6000), Units.PERCENT));
350                             }
351                         } else {
352                             logger.debug("maxSystemCurrent is 0. Ignoring.");
353                         }
354                         break;
355                     }
356                     case "Curr user": {
357                         int state = entry.getValue().getAsInt();
358                         maxPresetCurrent = state;
359                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
360                         updateState(CHANNEL_MAX_PRESET_CURRENT, newState);
361                         if (maxSystemCurrent != 0) {
362                             updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
363                                     Math.min(100, (state - 6000) * 100 / (maxSystemCurrent - 6000)), Units.PERCENT));
364                         }
365                         break;
366                     }
367                     case "Curr FS": {
368                         int state = entry.getValue().getAsInt();
369                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
370                         updateState(CHANNEL_FAILSAFE_CURRENT, newState);
371                         break;
372                     }
373                     case "Max curr": {
374                         int state = entry.getValue().getAsInt();
375                         maxPresetCurrent = state;
376                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
377                         updateState(CHANNEL_PILOT_CURRENT, newState);
378                         break;
379                     }
380                     case "Max curr %": {
381                         int state = entry.getValue().getAsInt();
382                         State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
383                         updateState(CHANNEL_PILOT_PWM, newState);
384                         break;
385                     }
386                     case "Output": {
387                         int state = entry.getValue().getAsInt();
388                         switch (state) {
389                             case 1: {
390                                 updateState(CHANNEL_OUTPUT, OnOffType.ON);
391                                 break;
392                             }
393                             default: {
394                                 updateState(CHANNEL_OUTPUT, OnOffType.OFF);
395                                 break;
396                             }
397                         }
398                         break;
399                     }
400                     case "Input": {
401                         int state = entry.getValue().getAsInt();
402                         switch (state) {
403                             case 1: {
404                                 updateState(CHANNEL_INPUT, OnOffType.ON);
405                                 break;
406                             }
407                             default: {
408                                 updateState(CHANNEL_INPUT, OnOffType.OFF);
409                                 break;
410                             }
411                         }
412                         break;
413                     }
414                     case "Sec": {
415                         long state = entry.getValue().getAsLong();
416                         State newState = new QuantityType<Time>(state, Units.SECOND);
417                         updateState(CHANNEL_UPTIME, newState);
418                         break;
419                     }
420                     case "U1": {
421                         int state = entry.getValue().getAsInt();
422                         State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
423                         updateState(CHANNEL_U1, newState);
424                         break;
425                     }
426                     case "U2": {
427                         int state = entry.getValue().getAsInt();
428                         State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
429                         updateState(CHANNEL_U2, newState);
430                         break;
431                     }
432                     case "U3": {
433                         int state = entry.getValue().getAsInt();
434                         State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
435                         updateState(CHANNEL_U3, newState);
436                         break;
437                     }
438                     case "I1": {
439                         int state = entry.getValue().getAsInt();
440                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
441                         updateState(CHANNEL_I1, newState);
442                         break;
443                     }
444                     case "I2": {
445                         int state = entry.getValue().getAsInt();
446                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
447                         updateState(CHANNEL_I2, newState);
448                         break;
449                     }
450                     case "I3": {
451                         int state = entry.getValue().getAsInt();
452                         State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
453                         updateState(CHANNEL_I3, newState);
454                         break;
455                     }
456                     case "P": {
457                         long state = entry.getValue().getAsLong();
458                         State newState = new QuantityType<Power>(state / 1000.0, Units.WATT);
459                         updateState(CHANNEL_POWER, newState);
460                         break;
461                     }
462                     case "PF": {
463                         int state = entry.getValue().getAsInt();
464                         State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
465                         updateState(CHANNEL_POWER_FACTOR, newState);
466                         break;
467                     }
468                     case "E pres": {
469                         long state = entry.getValue().getAsLong();
470                         State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
471                         updateState(CHANNEL_SESSION_CONSUMPTION, newState);
472                         break;
473                     }
474                     case "E total": {
475                         long state = entry.getValue().getAsLong();
476                         State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
477                         updateState(CHANNEL_TOTAL_CONSUMPTION, newState);
478                         break;
479                     }
480                     case "AuthON": {
481                         int state = entry.getValue().getAsInt();
482                         State newState = new DecimalType(state);
483                         updateState(CHANNEL_AUTHON, newState);
484                         break;
485                     }
486                     case "Authreq": {
487                         int state = entry.getValue().getAsInt();
488                         State newState = new DecimalType(state);
489                         updateState(CHANNEL_AUTHREQ, newState);
490                         break;
491                     }
492                     case "RFID tag": {
493                         String state = entry.getValue().getAsString().trim();
494                         State newState = new StringType(state);
495                         updateState(CHANNEL_SESSION_RFID_TAG, newState);
496                         break;
497                     }
498                     case "RFID class": {
499                         String state = entry.getValue().getAsString().trim();
500                         State newState = new StringType(state);
501                         updateState(CHANNEL_SESSION_RFID_CLASS, newState);
502                         break;
503                     }
504                     case "Session ID": {
505                         int state = entry.getValue().getAsInt();
506                         State newState = new DecimalType(state);
507                         updateState(CHANNEL_SESSION_SESSION_ID, newState);
508                         break;
509                     }
510                     case "Setenergy": {
511                         int state = entry.getValue().getAsInt();
512                         State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
513                         updateState(CHANNEL_SETENERGY, newState);
514                         break;
515                     }
516                 }
517             }
518         } catch (JsonParseException e) {
519             logger.debug("Invalid JSON data will be ignored: '{}'", response);
520         }
521     }
522
523     @Override
524     public void handleCommand(ChannelUID channelUID, Command command) {
525         if ((command instanceof RefreshType)) {
526             // let's assume we do frequent enough polling and ignore the REFRESH request here
527             // in order to prevent too many channel state updates
528         } else {
529             switch (channelUID.getId()) {
530                 case CHANNEL_MAX_PRESET_CURRENT: {
531                     if (command instanceof QuantityType<?> quantityCommand) {
532                         QuantityType<?> value = quantityCommand.toUnit("mA");
533
534                         transceiver.send("curr " + Math.min(Math.max(6000, value.intValue()), maxSystemCurrent), this);
535                     }
536                     break;
537                 }
538                 case CHANNEL_MAX_PRESET_CURRENT_RANGE: {
539                     if (command instanceof OnOffType || command instanceof IncreaseDecreaseType
540                             || command instanceof QuantityType<?>) {
541                         long newValue = 6000;
542                         if (command == IncreaseDecreaseType.INCREASE) {
543                             newValue = Math.min(Math.max(6000, maxPresetCurrent + 1), maxSystemCurrent);
544                         } else if (command == IncreaseDecreaseType.DECREASE) {
545                             newValue = Math.min(Math.max(6000, maxPresetCurrent - 1), maxSystemCurrent);
546                         } else if (command == OnOffType.ON) {
547                             newValue = maxSystemCurrent;
548                         } else if (command == OnOffType.OFF) {
549                             newValue = 6000;
550                         } else if (command instanceof QuantityType<?> quantityCommand) {
551                             QuantityType<?> value = quantityCommand.toUnit("%");
552                             newValue = Math.round(6000 + (maxSystemCurrent - 6000) * value.doubleValue() / 100.0);
553                         } else {
554                             return;
555                         }
556                         transceiver.send("curr " + newValue, this);
557                     }
558                     break;
559                 }
560                 case CHANNEL_ENABLED: {
561                     if (command instanceof OnOffType) {
562                         if (command == OnOffType.ON) {
563                             transceiver.send("ena 1", this);
564                         } else if (command == OnOffType.OFF) {
565                             transceiver.send("ena 0", this);
566                         } else {
567                             return;
568                         }
569                     }
570                     break;
571                 }
572                 case CHANNEL_OUTPUT: {
573                     if (command instanceof OnOffType) {
574                         if (command == OnOffType.ON) {
575                             transceiver.send("output 1", this);
576                         } else if (command == OnOffType.OFF) {
577                             transceiver.send("output 0", this);
578                         } else {
579                             return;
580                         }
581                     }
582                     break;
583                 }
584                 case CHANNEL_DISPLAY: {
585                     if (command instanceof StringType) {
586                         if (type == KebaType.P30 && (series == KebaSeries.C || series == KebaSeries.X)) {
587                             String cmd = command.toString();
588                             int maxLength = (cmd.length() < 23) ? cmd.length() : 23;
589                             transceiver.send("display 0 0 0 0 " + cmd.substring(0, maxLength), this);
590                         } else {
591                             logger.warn("'Display' is not supported on a KEBA KeContact {}:{}", type, series);
592                         }
593                     }
594                     break;
595                 }
596                 case CHANNEL_SETENERGY: {
597                     if (command instanceof QuantityType<?> quantityCommand) {
598                         QuantityType<?> value = quantityCommand.toUnit(Units.WATT_HOUR);
599                         transceiver.send(
600                                 "setenergy " + Math.min(Math.max(0, Math.round(value.doubleValue() * 10.0)), 999999999),
601                                 this);
602                     }
603                     break;
604                 }
605                 case CHANNEL_AUTHENTICATE: {
606                     if (command instanceof StringType) {
607                         String cmd = command.toString();
608                         // cmd must contain ID + CLASS (works only if the RFID TAG is in the whitelist of the Keba
609                         // station)
610                         transceiver.send("start " + cmd, this);
611                     }
612                     break;
613                 }
614             }
615         }
616     }
617 }