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