]> git.basschouten.com Git - openhab-addons.git/blob
a493587332ae5e48a0b826d5270bd5005840662a
[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.enocean.internal.handler;
14
15 import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.*;
16
17 import java.io.IOException;
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.LinkedList;
21 import java.util.Set;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.stream.Collectors;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.enocean.internal.EnOceanConfigStatusMessage;
29 import org.openhab.binding.enocean.internal.config.EnOceanBaseConfig;
30 import org.openhab.binding.enocean.internal.config.EnOceanBridgeConfig;
31 import org.openhab.binding.enocean.internal.config.EnOceanBridgeConfig.ESPVersion;
32 import org.openhab.binding.enocean.internal.messages.BasePacket;
33 import org.openhab.binding.enocean.internal.messages.ESP3PacketFactory;
34 import org.openhab.binding.enocean.internal.messages.Response;
35 import org.openhab.binding.enocean.internal.messages.Response.ResponseType;
36 import org.openhab.binding.enocean.internal.messages.responses.BaseResponse;
37 import org.openhab.binding.enocean.internal.messages.responses.RDBaseIdResponse;
38 import org.openhab.binding.enocean.internal.messages.responses.RDLearnedClientsResponse;
39 import org.openhab.binding.enocean.internal.messages.responses.RDLearnedClientsResponse.LearnedClient;
40 import org.openhab.binding.enocean.internal.messages.responses.RDRepeaterResponse;
41 import org.openhab.binding.enocean.internal.messages.responses.RDVersionResponse;
42 import org.openhab.binding.enocean.internal.transceiver.EnOceanESP2Transceiver;
43 import org.openhab.binding.enocean.internal.transceiver.EnOceanESP3Transceiver;
44 import org.openhab.binding.enocean.internal.transceiver.EnOceanTransceiver;
45 import org.openhab.binding.enocean.internal.transceiver.PacketListener;
46 import org.openhab.binding.enocean.internal.transceiver.ResponseListener;
47 import org.openhab.binding.enocean.internal.transceiver.ResponseListenerIgnoringTimeouts;
48 import org.openhab.binding.enocean.internal.transceiver.TeachInListener;
49 import org.openhab.binding.enocean.internal.transceiver.TransceiverErrorListener;
50 import org.openhab.core.config.core.Configuration;
51 import org.openhab.core.config.core.status.ConfigStatusMessage;
52 import org.openhab.core.io.transport.serial.PortInUseException;
53 import org.openhab.core.io.transport.serial.SerialPortManager;
54 import org.openhab.core.library.types.StringType;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.ThingTypeUID;
61 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.openhab.core.util.HexUtils;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 /**
69  * The {@link EnOceanBridgeHandler} is responsible for sending ESP3Packages build by
70  * {@link org.openhab.binding.enocean.internal.handler.EnOceanActuatorHandler} and
71  * transferring received ESP3Packages to
72  * {@link org.openhab.binding.enocean.internal.handler.EnOceanSensorHandler}.
73  *
74  * @author Daniel Weber - Initial contribution
75  */
76 @NonNullByDefault
77 public class EnOceanBridgeHandler extends ConfigStatusBridgeHandler implements TransceiverErrorListener {
78
79     private Logger logger = LoggerFactory.getLogger(EnOceanBridgeHandler.class);
80
81     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
82
83     private @Nullable EnOceanTransceiver transceiver; // holds connection to serial/tcp port and sends/receives messages
84     private @Nullable ScheduledFuture<?> connectorTask; // is used for reconnection if something goes wrong
85
86     private byte[] baseId = new byte[0];
87     private Thing[] sendingThings = new Thing[128];
88
89     private SerialPortManager serialPortManager;
90
91     private boolean smackAvailable = false;
92     private boolean sendTeachOuts = true;
93     private Set<String> smackClients = Set.of();
94
95     public EnOceanBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) {
96         super(bridge);
97         this.serialPortManager = serialPortManager;
98     }
99
100     @Override
101     public void handleCommand(ChannelUID channelUID, Command command) {
102         if (transceiver == null) {
103             updateStatus(ThingStatus.OFFLINE);
104             return;
105         }
106
107         switch (channelUID.getId()) {
108             case CHANNEL_REPEATERMODE:
109                 if (command instanceof RefreshType) {
110                     sendMessage(ESP3PacketFactory.CO_RD_REPEATER,
111                             new ResponseListenerIgnoringTimeouts<RDRepeaterResponse>() {
112                                 @Override
113                                 public void responseReceived(RDRepeaterResponse response) {
114                                     if (response.isValid() && response.isOK()) {
115                                         updateState(channelUID, response.getRepeaterLevel());
116                                     } else {
117                                         updateState(channelUID, new StringType(REPEATERMODE_OFF));
118                                     }
119                                 }
120                             });
121                 } else if (command instanceof StringType stringCommand) {
122                     sendMessage(ESP3PacketFactory.CO_WR_REPEATER(stringCommand),
123                             new ResponseListenerIgnoringTimeouts<BaseResponse>() {
124                                 @Override
125                                 public void responseReceived(BaseResponse response) {
126                                     if (response.isOK()) {
127                                         updateState(channelUID, stringCommand);
128                                     }
129                                 }
130                             });
131                 }
132                 break;
133
134             case CHANNEL_SETBASEID:
135                 if (command instanceof StringType stringCommand) {
136                     try {
137                         byte[] id = HexUtils.hexToBytes(stringCommand.toFullString());
138
139                         sendMessage(ESP3PacketFactory.CO_WR_IDBASE(id),
140                                 new ResponseListenerIgnoringTimeouts<BaseResponse>() {
141                                     @Override
142                                     public void responseReceived(BaseResponse response) {
143                                         if (response.isOK()) {
144                                             updateState(channelUID, new StringType("New Id successfully set"));
145                                         } else if (response.getResponseType() == ResponseType.RET_FLASH_HW_ERROR) {
146                                             updateState(channelUID,
147                                                     new StringType("The write/erase/verify process failed"));
148                                         } else if (response.getResponseType() == ResponseType.RET_BASEID_OUT_OF_RANGE) {
149                                             updateState(channelUID, new StringType("Base id out of range"));
150                                         } else if (response.getResponseType() == ResponseType.RET_BASEID_MAX_REACHED) {
151                                             updateState(channelUID, new StringType("No more change possible"));
152                                         }
153                                     }
154                                 });
155                     } catch (IllegalArgumentException e) {
156                         updateState(channelUID, new StringType("BaseId could not be parsed"));
157                     }
158                 }
159                 break;
160
161             default:
162                 break;
163         }
164     }
165
166     @Override
167     public void initialize() {
168         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "trying to connect to gateway...");
169
170         connectorTask = scheduler.scheduleWithFixedDelay(new Runnable() {
171             @Override
172             public void run() {
173                 if (thing.getStatus() != ThingStatus.ONLINE) {
174                     initTransceiver();
175                 }
176             }
177         }, 0, 60, TimeUnit.SECONDS);
178     }
179
180     private synchronized void initTransceiver() {
181         try {
182             EnOceanBridgeConfig c = getThing().getConfiguration().as(EnOceanBridgeConfig.class);
183             EnOceanTransceiver localTransceiver = transceiver;
184             if (localTransceiver != null) {
185                 localTransceiver.shutDown();
186             }
187
188             switch (c.getESPVersion()) {
189                 case ESP2:
190                     transceiver = new EnOceanESP2Transceiver(c.path, this, scheduler, serialPortManager);
191                     smackAvailable = false;
192                     sendTeachOuts = false;
193                     break;
194                 case ESP3:
195                     transceiver = new EnOceanESP3Transceiver(c.path, this, scheduler, serialPortManager);
196                     sendTeachOuts = c.sendTeachOuts;
197                     break;
198                 default:
199                     break;
200             }
201
202             localTransceiver = transceiver;
203             if (localTransceiver == null) {
204                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
205                         "Failed to initialize EnOceanTransceiver");
206                 return;
207             }
208
209             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "opening serial port...");
210             localTransceiver.initialize();
211
212             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "starting rx thread...");
213             localTransceiver.startReceiving(scheduler);
214             logger.info("EnOceanSerialTransceiver RX thread up and running");
215
216             if (c.rs485) {
217                 if (!c.rs485BaseId.isEmpty()) {
218                     baseId = HexUtils.hexToBytes(c.rs485BaseId);
219                     if (baseId.length != 4) {
220                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
221                                 "RS485 BaseId has the wrong format. It is expected to be an 8 digit hex code, for example 01000000");
222                     }
223                 } else {
224                     baseId = new byte[4];
225                 }
226
227                 updateProperty(PROPERTY_BASE_ID, HexUtils.bytesToHex(baseId));
228                 updateStatus(ThingStatus.ONLINE);
229             } else {
230                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
231                         "trying to get bridge base id...");
232
233                 logger.debug("request base id");
234                 localTransceiver.sendBasePacket(ESP3PacketFactory.CO_RD_IDBASE,
235                         new ResponseListenerIgnoringTimeouts<RDBaseIdResponse>() {
236
237                             @Override
238                             public void responseReceived(RDBaseIdResponse response) {
239                                 logger.debug("received response for base id");
240                                 if (response.isValid() && response.isOK()) {
241                                     baseId = response.getBaseId().clone();
242                                     updateProperty(PROPERTY_BASE_ID, HexUtils.bytesToHex(response.getBaseId()));
243                                     updateProperty(PROPERTY_REMAINING_WRITE_CYCLES_BASE_ID,
244                                             Integer.toString(response.getRemainingWriteCycles()));
245                                     EnOceanTransceiver localTransceiver = transceiver;
246                                     if (localTransceiver != null) {
247                                         localTransceiver.setFilteredDeviceId(baseId);
248                                     }
249                                     updateStatus(ThingStatus.ONLINE);
250                                 } else {
251                                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
252                                             "Could not get BaseId");
253                                 }
254                             }
255                         });
256
257                 if (c.getESPVersion() == ESPVersion.ESP3) {
258                     logger.debug("set postmaster mailboxes");
259                     localTransceiver.sendBasePacket(ESP3PacketFactory.SA_WR_POSTMASTER((byte) (c.enableSmack ? 20 : 0)),
260                             new ResponseListenerIgnoringTimeouts<BaseResponse>() {
261
262                                 @Override
263                                 public void responseReceived(BaseResponse response) {
264                                     logger.debug("received response for postmaster mailboxes");
265                                     if (response.isOK()) {
266                                         updateProperty("Postmaster mailboxes:",
267                                                 Integer.toString(c.enableSmack ? 20 : 0));
268                                         smackAvailable = c.enableSmack;
269                                         refreshProperties();
270                                     } else {
271                                         updateProperty("Postmaster mailboxes:", "Not supported");
272                                         smackAvailable = false;
273                                     }
274                                 }
275                             });
276                 }
277             }
278
279             logger.debug("request version info");
280             localTransceiver.sendBasePacket(ESP3PacketFactory.CO_RD_VERSION,
281                     new ResponseListenerIgnoringTimeouts<RDVersionResponse>() {
282
283                         @Override
284                         public void responseReceived(RDVersionResponse response) {
285                             if (response.isValid() && response.isOK()) {
286                                 updateProperty(PROPERTY_APP_VERSION, response.getAPPVersion());
287                                 updateProperty(PROPERTY_API_VERSION, response.getAPIVersion());
288                                 updateProperty(PROPERTY_CHIP_ID, response.getChipID());
289                                 updateProperty(PROPERTY_DESCRIPTION, response.getDescription());
290                             }
291                         }
292                     });
293         } catch (IOException e) {
294             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Port could not be found");
295         } catch (PortInUseException e) {
296             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Port already in use");
297         } catch (Exception e) {
298             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Port could not be initialized");
299             return;
300         }
301     }
302
303     @Override
304     public synchronized void dispose() {
305         EnOceanTransceiver transceiver = this.transceiver;
306         if (transceiver != null) {
307             transceiver.shutDown();
308             this.transceiver = null;
309         }
310
311         ScheduledFuture<?> connectorTask = this.connectorTask;
312         if (connectorTask != null) {
313             connectorTask.cancel(true);
314             this.connectorTask = null;
315         }
316
317         super.dispose();
318     }
319
320     @Override
321     public Collection<ConfigStatusMessage> getConfigStatus() {
322         Collection<ConfigStatusMessage> configStatusMessages = new LinkedList<>();
323
324         // The serial port must be provided
325         String path = getThing().getConfiguration().as(EnOceanBridgeConfig.class).path;
326         if (path.isEmpty()) {
327             ConfigStatusMessage statusMessage = ConfigStatusMessage.Builder.error(PATH)
328                     .withMessageKeySuffix(EnOceanConfigStatusMessage.PORT_MISSING.getMessageKey()).withArguments(PATH)
329                     .build();
330             if (statusMessage != null) {
331                 configStatusMessages.add(statusMessage);
332             }
333         }
334
335         return configStatusMessages;
336     }
337
338     public byte[] getBaseId() {
339         return baseId.clone();
340     }
341
342     public boolean isSmackClient(Thing sender) {
343         return smackClients.contains(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId);
344     }
345
346     public @Nullable Integer getNextSenderId(Thing sender) {
347         return getNextSenderId(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId);
348     }
349
350     public @Nullable Integer getNextSenderId(String enoceanId) {
351         EnOceanBridgeConfig config = getConfigAs(EnOceanBridgeConfig.class);
352         Integer senderId = config.nextSenderId;
353         if (senderId == null) {
354             return null;
355         }
356         if (sendingThings[senderId] == null) {
357             Configuration c = this.editConfiguration();
358             c.put(PARAMETER_NEXT_SENDERID, null);
359             updateConfiguration(c);
360
361             return senderId;
362         }
363
364         for (int i = 1; i < sendingThings.length; i++) {
365             if (sendingThings[i] == null || sendingThings[i].getConfiguration().as(EnOceanBaseConfig.class).enoceanId
366                     .equalsIgnoreCase(enoceanId)) {
367                 return i;
368             }
369         }
370
371         return null;
372     }
373
374     public boolean existsSender(int id, Thing sender) {
375         return sendingThings[id] != null && !sendingThings[id].getConfiguration().as(EnOceanBaseConfig.class).enoceanId
376                 .equalsIgnoreCase(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId);
377     }
378
379     public void addSender(int id, Thing thing) {
380         sendingThings[id] = thing;
381     }
382
383     public void removeSender(int id) {
384         sendingThings[id] = null;
385     }
386
387     public <T extends @Nullable Response> void sendMessage(BasePacket message,
388             @Nullable ResponseListener<T> responseListener) {
389         try {
390             EnOceanTransceiver localTransceiver = transceiver;
391             if (localTransceiver == null) {
392                 throw new IOException("EnOceanTransceiver has state null");
393             }
394             localTransceiver.sendBasePacket(message, responseListener);
395         } catch (IOException e) {
396             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
397         }
398     }
399
400     public void addPacketListener(PacketListener listener) {
401         addPacketListener(listener, listener.getEnOceanIdToListenTo());
402     }
403
404     public void addPacketListener(PacketListener listener, long senderIdToListenTo) {
405         EnOceanTransceiver localTransceiver = transceiver;
406         if (localTransceiver != null) {
407             localTransceiver.addPacketListener(listener, senderIdToListenTo);
408         }
409     }
410
411     public void removePacketListener(PacketListener listener) {
412         removePacketListener(listener, listener.getEnOceanIdToListenTo());
413     }
414
415     public void removePacketListener(PacketListener listener, long senderIdToListenTo) {
416         EnOceanTransceiver localTransceiver = transceiver;
417         if (localTransceiver != null) {
418             localTransceiver.removePacketListener(listener, senderIdToListenTo);
419         }
420     }
421
422     public void startDiscovery(TeachInListener teachInListener) {
423         EnOceanTransceiver localTransceiver = transceiver;
424         if (localTransceiver != null) {
425             localTransceiver.startDiscovery(teachInListener);
426         }
427
428         if (smackAvailable) {
429             // activate smack teach in
430             logger.debug("activate smack teach in");
431             try {
432                 if (localTransceiver == null) {
433                     throw new IOException("EnOceanTransceiver has state null");
434                 }
435                 localTransceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(true),
436                         new ResponseListenerIgnoringTimeouts<BaseResponse>() {
437                             @Override
438                             public void responseReceived(BaseResponse response) {
439                                 if (response.isOK()) {
440                                     logger.debug("Smack teach in activated");
441                                 }
442                             }
443                         });
444             } catch (IOException e) {
445                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
446                         "Smack packet could not be send: " + e.getMessage());
447             }
448         }
449     }
450
451     public void stopDiscovery() {
452         EnOceanTransceiver localTransceiver = transceiver;
453         if (localTransceiver != null) {
454             localTransceiver.stopDiscovery();
455         }
456
457         try {
458             if (localTransceiver == null) {
459                 throw new IOException("EnOceanTransceiver has state null");
460             }
461             localTransceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(false), null);
462             refreshProperties();
463         } catch (IOException e) {
464             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
465                     "Smack packet could not be send: " + e.getMessage());
466         }
467     }
468
469     private void refreshProperties() {
470         if (getThing().getStatus() == ThingStatus.ONLINE && smackAvailable) {
471             logger.debug("request learned smack clients");
472             try {
473                 EnOceanTransceiver localTransceiver = transceiver;
474                 if (localTransceiver != null) {
475                     localTransceiver.sendBasePacket(ESP3PacketFactory.SA_RD_LEARNEDCLIENTS,
476                             new ResponseListenerIgnoringTimeouts<RDLearnedClientsResponse>() {
477                                 @Override
478                                 public void responseReceived(RDLearnedClientsResponse response) {
479                                     logger.debug("received response for learned smack clients");
480                                     if (response.isValid() && response.isOK()) {
481                                         LearnedClient[] clients = response.getLearnedClients();
482                                         updateProperty("Learned smart ack clients", Integer.toString(clients.length));
483                                         updateProperty("Smart ack clients",
484                                                 Arrays.stream(clients)
485                                                         .map(x -> String.format("%s (MB Idx: %d)",
486                                                                 HexUtils.bytesToHex(x.clientId), x.mailboxIndex))
487                                                         .collect(Collectors.joining(", ")));
488                                         smackClients = Arrays.stream(clients).map(x -> HexUtils.bytesToHex(x.clientId))
489                                                 .collect(Collectors.toSet());
490                                     }
491                                 }
492                             });
493                 }
494             } catch (IOException e) {
495                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
496                         "Smack packet could not be send: " + e.getMessage());
497             }
498         }
499     }
500
501     @Override
502     public void errorOccured(Throwable exception) {
503         EnOceanTransceiver localTransceiver = transceiver;
504         if (localTransceiver != null) {
505             localTransceiver.shutDown();
506             transceiver = null;
507         }
508         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, exception.getMessage());
509     }
510
511     public boolean sendTeachOuts() {
512         return sendTeachOuts;
513     }
514 }