]> git.basschouten.com Git - openhab-addons.git/blob
0a4cb5f3f8367fde500ef1c743d8f8964424235c
[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 EnOceanActuatorHandler} and
70  * transferring received ESP3Packages to {@link EnOceanSensorHandler}.
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) {
120                     sendMessage(ESP3PacketFactory.CO_WR_REPEATER((StringType) command),
121                             new ResponseListenerIgnoringTimeouts<BaseResponse>() {
122                                 @Override
123                                 public void responseReceived(BaseResponse response) {
124                                     if (response.isOK()) {
125                                         updateState(channelUID, (StringType) command);
126                                     }
127                                 }
128                             });
129                 }
130                 break;
131
132             case CHANNEL_SETBASEID:
133                 if (command instanceof StringType) {
134                     try {
135                         byte[] id = HexUtils.hexToBytes(((StringType) command).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         ScheduledFuture<?> localConnectorTask = connectorTask;
169         if (localConnectorTask == null || localConnectorTask.isDone()) {
170             localConnectorTask = 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
181     private synchronized void initTransceiver() {
182         try {
183             EnOceanBridgeConfig c = getThing().getConfiguration().as(EnOceanBridgeConfig.class);
184             EnOceanTransceiver localTransceiver = transceiver;
185             if (localTransceiver != null) {
186                 localTransceiver.shutDown();
187             }
188
189             switch (c.getESPVersion()) {
190                 case ESP2:
191                     transceiver = new EnOceanESP2Transceiver(c.path, this, scheduler, serialPortManager);
192                     smackAvailable = false;
193                     sendTeachOuts = false;
194                     break;
195                 case ESP3:
196                     transceiver = new EnOceanESP3Transceiver(c.path, this, scheduler, serialPortManager);
197                     sendTeachOuts = c.sendTeachOuts;
198                     break;
199                 default:
200                     break;
201             }
202
203             localTransceiver = transceiver;
204             if (localTransceiver == null) {
205                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
206                         "Failed to initialize EnOceanTransceiver");
207                 return;
208             }
209
210             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "opening serial port...");
211             localTransceiver.initialize();
212
213             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "starting rx thread...");
214             localTransceiver.startReceiving(scheduler);
215             logger.info("EnOceanSerialTransceiver RX thread up and running");
216
217             if (c.rs485) {
218                 if (!c.rs485BaseId.isEmpty()) {
219                     baseId = HexUtils.hexToBytes(c.rs485BaseId);
220                     if (baseId.length != 4) {
221                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
222                                 "RS485 BaseId has the wrong format. It is expected to be an 8 digit hex code, for example 01000000");
223                     }
224                 } else {
225                     baseId = new byte[4];
226                 }
227
228                 updateProperty(PROPERTY_BASE_ID, HexUtils.bytesToHex(baseId));
229                 updateStatus(ThingStatus.ONLINE);
230             } else {
231                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
232                         "trying to get bridge base id...");
233
234                 logger.debug("request base id");
235                 localTransceiver.sendBasePacket(ESP3PacketFactory.CO_RD_IDBASE,
236                         new ResponseListenerIgnoringTimeouts<RDBaseIdResponse>() {
237
238                             @Override
239                             public void responseReceived(RDBaseIdResponse response) {
240                                 logger.debug("received response for base id");
241                                 if (response.isValid() && response.isOK()) {
242                                     baseId = response.getBaseId().clone();
243                                     updateProperty(PROPERTY_BASE_ID, HexUtils.bytesToHex(response.getBaseId()));
244                                     updateProperty(PROPERTY_REMAINING_WRITE_CYCLES_BASE_ID,
245                                             Integer.toString(response.getRemainingWriteCycles()));
246                                     EnOceanTransceiver localTransceiver = transceiver;
247                                     if (localTransceiver != null) {
248                                         localTransceiver.setFilteredDeviceId(baseId);
249                                     }
250                                     updateStatus(ThingStatus.ONLINE);
251                                 } else {
252                                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
253                                             "Could not get BaseId");
254                                 }
255                             }
256                         });
257
258                 if (c.getESPVersion() == ESPVersion.ESP3) {
259                     logger.debug("set postmaster mailboxes");
260                     localTransceiver.sendBasePacket(ESP3PacketFactory.SA_WR_POSTMASTER((byte) (c.enableSmack ? 20 : 0)),
261                             new ResponseListenerIgnoringTimeouts<BaseResponse>() {
262
263                                 @Override
264                                 public void responseReceived(BaseResponse response) {
265                                     logger.debug("received response for postmaster mailboxes");
266                                     if (response.isOK()) {
267                                         updateProperty("Postmaster mailboxes:",
268                                                 Integer.toString(c.enableSmack ? 20 : 0));
269                                         smackAvailable = c.enableSmack;
270                                         refreshProperties();
271                                     } else {
272                                         updateProperty("Postmaster mailboxes:", "Not supported");
273                                         smackAvailable = false;
274                                     }
275                                 }
276                             });
277                 }
278             }
279
280             logger.debug("request version info");
281             localTransceiver.sendBasePacket(ESP3PacketFactory.CO_RD_VERSION,
282                     new ResponseListenerIgnoringTimeouts<RDVersionResponse>() {
283
284                         @Override
285                         public void responseReceived(RDVersionResponse response) {
286                             if (response.isValid() && response.isOK()) {
287                                 updateProperty(PROPERTY_APP_VERSION, response.getAPPVersion());
288                                 updateProperty(PROPERTY_API_VERSION, response.getAPIVersion());
289                                 updateProperty(PROPERTY_CHIP_ID, response.getChipID());
290                                 updateProperty(PROPERTY_DESCRIPTION, response.getDescription());
291                             }
292                         }
293                     });
294         } catch (IOException e) {
295             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Port could not be found");
296         } catch (PortInUseException e) {
297             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Port already in use");
298         } catch (Exception e) {
299             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Port could not be initialized");
300             return;
301         }
302     }
303
304     @Override
305     public synchronized void dispose() {
306         EnOceanTransceiver localTransceiver = transceiver;
307         if (localTransceiver != null) {
308             localTransceiver.shutDown();
309             transceiver = null;
310         }
311
312         ScheduledFuture<?> localConnectorTask = connectorTask;
313         if (localConnectorTask != null && !localConnectorTask.isDone()) {
314             localConnectorTask.cancel(true);
315             connectorTask = null;
316         }
317
318         super.dispose();
319     }
320
321     @Override
322     public Collection<ConfigStatusMessage> getConfigStatus() {
323         Collection<ConfigStatusMessage> configStatusMessages = new LinkedList<>();
324
325         // The serial port must be provided
326         String path = getThing().getConfiguration().as(EnOceanBridgeConfig.class).path;
327         if (path.isEmpty()) {
328             ConfigStatusMessage statusMessage = ConfigStatusMessage.Builder.error(PATH)
329                     .withMessageKeySuffix(EnOceanConfigStatusMessage.PORT_MISSING.getMessageKey()).withArguments(PATH)
330                     .build();
331             if (statusMessage != null) {
332                 configStatusMessages.add(statusMessage);
333             }
334         }
335
336         return configStatusMessages;
337     }
338
339     public byte[] getBaseId() {
340         return baseId.clone();
341     }
342
343     public boolean isSmackClient(Thing sender) {
344         return smackClients.contains(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId);
345     }
346
347     public @Nullable Integer getNextSenderId(Thing sender) {
348         return getNextSenderId(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId);
349     }
350
351     public @Nullable Integer getNextSenderId(String enoceanId) {
352         EnOceanBridgeConfig config = getConfigAs(EnOceanBridgeConfig.class);
353         Integer senderId = config.nextSenderId;
354         if (senderId == null) {
355             return null;
356         }
357         if (sendingThings[senderId] == null) {
358             Configuration c = this.editConfiguration();
359             c.put(PARAMETER_NEXT_SENDERID, null);
360             updateConfiguration(c);
361
362             return senderId;
363         }
364
365         for (int i = 1; i < sendingThings.length; i++) {
366             if (sendingThings[i] == null || sendingThings[i].getConfiguration().as(EnOceanBaseConfig.class).enoceanId
367                     .equalsIgnoreCase(enoceanId)) {
368                 return i;
369             }
370         }
371
372         return null;
373     }
374
375     public boolean existsSender(int id, Thing sender) {
376         return sendingThings[id] != null && !sendingThings[id].getConfiguration().as(EnOceanBaseConfig.class).enoceanId
377                 .equalsIgnoreCase(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId);
378     }
379
380     public void addSender(int id, Thing thing) {
381         sendingThings[id] = thing;
382     }
383
384     public void removeSender(int id) {
385         sendingThings[id] = null;
386     }
387
388     public <T extends @Nullable Response> void sendMessage(BasePacket message,
389             @Nullable ResponseListener<T> responseListener) {
390         try {
391             EnOceanTransceiver localTransceiver = transceiver;
392             if (localTransceiver == null) {
393                 throw new IOException("EnOceanTransceiver has state null");
394             }
395             localTransceiver.sendBasePacket(message, responseListener);
396         } catch (IOException e) {
397             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
398         }
399     }
400
401     public void addPacketListener(PacketListener listener) {
402         addPacketListener(listener, listener.getEnOceanIdToListenTo());
403     }
404
405     public void addPacketListener(PacketListener listener, long senderIdToListenTo) {
406         EnOceanTransceiver localTransceiver = transceiver;
407         if (localTransceiver != null) {
408             localTransceiver.addPacketListener(listener, senderIdToListenTo);
409         }
410     }
411
412     public void removePacketListener(PacketListener listener) {
413         removePacketListener(listener, listener.getEnOceanIdToListenTo());
414     }
415
416     public void removePacketListener(PacketListener listener, long senderIdToListenTo) {
417         EnOceanTransceiver localTransceiver = transceiver;
418         if (localTransceiver != null) {
419             localTransceiver.removePacketListener(listener, senderIdToListenTo);
420         }
421     }
422
423     public void startDiscovery(TeachInListener teachInListener) {
424         EnOceanTransceiver localTransceiver = transceiver;
425         if (localTransceiver != null) {
426             localTransceiver.startDiscovery(teachInListener);
427         }
428
429         if (smackAvailable) {
430             // activate smack teach in
431             logger.debug("activate smack teach in");
432             try {
433                 if (localTransceiver == null) {
434                     throw new IOException("EnOceanTransceiver has state null");
435                 }
436                 localTransceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(true),
437                         new ResponseListenerIgnoringTimeouts<BaseResponse>() {
438                             @Override
439                             public void responseReceived(BaseResponse response) {
440                                 if (response.isOK()) {
441                                     logger.debug("Smack teach in activated");
442                                 }
443                             }
444                         });
445             } catch (IOException e) {
446                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
447                         "Smack packet could not be send: " + e.getMessage());
448             }
449         }
450     }
451
452     public void stopDiscovery() {
453         EnOceanTransceiver localTransceiver = transceiver;
454         if (localTransceiver != null) {
455             localTransceiver.stopDiscovery();
456         }
457
458         try {
459             if (localTransceiver == null) {
460                 throw new IOException("EnOceanTransceiver has state null");
461             }
462             localTransceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(false), null);
463             refreshProperties();
464         } catch (IOException e) {
465             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
466                     "Smack packet could not be send: " + e.getMessage());
467         }
468     }
469
470     private void refreshProperties() {
471         if (getThing().getStatus() == ThingStatus.ONLINE && smackAvailable) {
472             logger.debug("request learned smack clients");
473             try {
474                 EnOceanTransceiver localTransceiver = transceiver;
475                 if (localTransceiver != null) {
476                     localTransceiver.sendBasePacket(ESP3PacketFactory.SA_RD_LEARNEDCLIENTS,
477                             new ResponseListenerIgnoringTimeouts<RDLearnedClientsResponse>() {
478                                 @Override
479                                 public void responseReceived(RDLearnedClientsResponse response) {
480                                     logger.debug("received response for learned smack clients");
481                                     if (response.isValid() && response.isOK()) {
482                                         LearnedClient[] clients = response.getLearnedClients();
483                                         updateProperty("Learned smart ack clients", Integer.toString(clients.length));
484                                         updateProperty("Smart ack clients",
485                                                 Arrays.stream(clients)
486                                                         .map(x -> String.format("%s (MB Idx: %d)",
487                                                                 HexUtils.bytesToHex(x.clientId), x.mailboxIndex))
488                                                         .collect(Collectors.joining(", ")));
489                                         smackClients = Arrays.stream(clients).map(x -> HexUtils.bytesToHex(x.clientId))
490                                                 .collect(Collectors.toSet());
491                                     }
492                                 }
493                             });
494                 }
495             } catch (IOException e) {
496                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
497                         "Smack packet could not be send: " + e.getMessage());
498             }
499         }
500     }
501
502     @Override
503     public void errorOccured(Throwable exception) {
504         EnOceanTransceiver localTransceiver = transceiver;
505         if (localTransceiver != null) {
506             localTransceiver.shutDown();
507             transceiver = null;
508         }
509         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, exception.getMessage());
510     }
511
512     public boolean sendTeachOuts() {
513         return sendTeachOuts;
514     }
515 }