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