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