2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.enocean.internal.handler;
15 import static org.openhab.binding.enocean.internal.EnOceanBindingConstants.*;
17 import java.io.IOException;
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.LinkedList;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.stream.Collectors;
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;
69 * The {@link EnOceanBridgeHandler} is responsible for sending ESP3Packages build by {@link EnOceanActuatorHandler} and
70 * transferring received ESP3Packages to {@link EnOceanSensorHandler}.
72 * @author Daniel Weber - Initial contribution
75 public class EnOceanBridgeHandler extends ConfigStatusBridgeHandler implements TransceiverErrorListener {
77 private Logger logger = LoggerFactory.getLogger(EnOceanBridgeHandler.class);
79 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
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
84 private byte[] baseId = new byte[0];
85 private Thing[] sendingThings = new Thing[128];
87 private SerialPortManager serialPortManager;
89 private boolean smackAvailable = false;
90 private boolean sendTeachOuts = true;
91 private Set<String> smackClients = Set.of();
93 public EnOceanBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) {
95 this.serialPortManager = serialPortManager;
99 public void handleCommand(ChannelUID channelUID, Command command) {
100 if (transceiver == null) {
101 updateStatus(ThingStatus.OFFLINE);
105 switch (channelUID.getId()) {
106 case CHANNEL_REPEATERMODE:
107 if (command instanceof RefreshType) {
108 sendMessage(ESP3PacketFactory.CO_RD_REPEATER,
109 new ResponseListenerIgnoringTimeouts<RDRepeaterResponse>() {
111 public void responseReceived(RDRepeaterResponse response) {
112 if (response.isValid() && response.isOK()) {
113 updateState(channelUID, response.getRepeaterLevel());
115 updateState(channelUID, new StringType(REPEATERMODE_OFF));
119 } else if (command instanceof StringType) {
120 sendMessage(ESP3PacketFactory.CO_WR_REPEATER((StringType) command),
121 new ResponseListenerIgnoringTimeouts<BaseResponse>() {
123 public void responseReceived(BaseResponse response) {
124 if (response.isOK()) {
125 updateState(channelUID, (StringType) command);
132 case CHANNEL_SETBASEID:
133 if (command instanceof StringType) {
135 byte[] id = HexUtils.hexToBytes(((StringType) command).toFullString());
137 sendMessage(ESP3PacketFactory.CO_WR_IDBASE(id),
138 new ResponseListenerIgnoringTimeouts<BaseResponse>() {
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"));
153 } catch (IllegalArgumentException e) {
154 updateState(channelUID, new StringType("BaseId could not be parsed"));
165 public void initialize() {
166 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "trying to connect to gateway...");
168 ScheduledFuture<?> localConnectorTask = connectorTask;
169 if (localConnectorTask == null || localConnectorTask.isDone()) {
170 localConnectorTask = scheduler.scheduleWithFixedDelay(new Runnable() {
173 if (thing.getStatus() != ThingStatus.ONLINE) {
177 }, 0, 60, TimeUnit.SECONDS);
181 private synchronized void initTransceiver() {
183 EnOceanBridgeConfig c = getThing().getConfiguration().as(EnOceanBridgeConfig.class);
184 EnOceanTransceiver localTransceiver = transceiver;
185 if (localTransceiver != null) {
186 localTransceiver.shutDown();
189 switch (c.getESPVersion()) {
191 transceiver = new EnOceanESP2Transceiver(c.path, this, scheduler, serialPortManager);
192 smackAvailable = false;
193 sendTeachOuts = false;
196 transceiver = new EnOceanESP3Transceiver(c.path, this, scheduler, serialPortManager);
197 sendTeachOuts = c.sendTeachOuts;
203 localTransceiver = transceiver;
204 if (localTransceiver == null) {
205 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
206 "Failed to initialize EnOceanTransceiver");
210 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "opening serial port...");
211 localTransceiver.initialize();
213 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "starting rx thread...");
214 localTransceiver.startReceiving(scheduler);
215 logger.info("EnOceanSerialTransceiver RX thread up and running");
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");
225 baseId = new byte[4];
228 updateProperty(PROPERTY_BASE_ID, HexUtils.bytesToHex(baseId));
229 updateStatus(ThingStatus.ONLINE);
231 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
232 "trying to get bridge base id...");
234 logger.debug("request base id");
235 localTransceiver.sendBasePacket(ESP3PacketFactory.CO_RD_IDBASE,
236 new ResponseListenerIgnoringTimeouts<RDBaseIdResponse>() {
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);
250 updateStatus(ThingStatus.ONLINE);
252 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
253 "Could not get BaseId");
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>() {
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;
272 updateProperty("Postmaster mailboxes:", "Not supported");
273 smackAvailable = false;
280 logger.debug("request version info");
281 localTransceiver.sendBasePacket(ESP3PacketFactory.CO_RD_VERSION,
282 new ResponseListenerIgnoringTimeouts<RDVersionResponse>() {
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());
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");
305 public synchronized void dispose() {
306 EnOceanTransceiver localTransceiver = transceiver;
307 if (localTransceiver != null) {
308 localTransceiver.shutDown();
312 ScheduledFuture<?> localConnectorTask = connectorTask;
313 if (localConnectorTask != null && !localConnectorTask.isDone()) {
314 localConnectorTask.cancel(true);
315 connectorTask = null;
322 public Collection<ConfigStatusMessage> getConfigStatus() {
323 Collection<ConfigStatusMessage> configStatusMessages = new LinkedList<>();
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)
331 if (statusMessage != null) {
332 configStatusMessages.add(statusMessage);
336 return configStatusMessages;
339 public byte[] getBaseId() {
340 return baseId.clone();
343 public boolean isSmackClient(Thing sender) {
344 return smackClients.contains(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId);
347 public @Nullable Integer getNextSenderId(Thing sender) {
348 return getNextSenderId(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId);
351 public @Nullable Integer getNextSenderId(String enoceanId) {
352 EnOceanBridgeConfig config = getConfigAs(EnOceanBridgeConfig.class);
353 Integer senderId = config.nextSenderId;
354 if (senderId == null) {
357 if (sendingThings[senderId] == null) {
358 Configuration c = this.editConfiguration();
359 c.put(PARAMETER_NEXT_SENDERID, null);
360 updateConfiguration(c);
365 for (int i = 1; i < sendingThings.length; i++) {
366 if (sendingThings[i] == null || sendingThings[i].getConfiguration().as(EnOceanBaseConfig.class).enoceanId
367 .equalsIgnoreCase(enoceanId)) {
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);
380 public void addSender(int id, Thing thing) {
381 sendingThings[id] = thing;
384 public void removeSender(int id) {
385 sendingThings[id] = null;
388 public <T extends @Nullable Response> void sendMessage(BasePacket message,
389 @Nullable ResponseListener<T> responseListener) {
391 EnOceanTransceiver localTransceiver = transceiver;
392 if (localTransceiver == null) {
393 throw new IOException("EnOceanTransceiver has state null");
395 localTransceiver.sendBasePacket(message, responseListener);
396 } catch (IOException e) {
397 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
401 public void addPacketListener(PacketListener listener) {
402 addPacketListener(listener, listener.getEnOceanIdToListenTo());
405 public void addPacketListener(PacketListener listener, long senderIdToListenTo) {
406 EnOceanTransceiver localTransceiver = transceiver;
407 if (localTransceiver != null) {
408 localTransceiver.addPacketListener(listener, senderIdToListenTo);
412 public void removePacketListener(PacketListener listener) {
413 removePacketListener(listener, listener.getEnOceanIdToListenTo());
416 public void removePacketListener(PacketListener listener, long senderIdToListenTo) {
417 EnOceanTransceiver localTransceiver = transceiver;
418 if (localTransceiver != null) {
419 localTransceiver.removePacketListener(listener, senderIdToListenTo);
423 public void startDiscovery(TeachInListener teachInListener) {
424 EnOceanTransceiver localTransceiver = transceiver;
425 if (localTransceiver != null) {
426 localTransceiver.startDiscovery(teachInListener);
429 if (smackAvailable) {
430 // activate smack teach in
431 logger.debug("activate smack teach in");
433 if (localTransceiver == null) {
434 throw new IOException("EnOceanTransceiver has state null");
436 localTransceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(true),
437 new ResponseListenerIgnoringTimeouts<BaseResponse>() {
439 public void responseReceived(BaseResponse response) {
440 if (response.isOK()) {
441 logger.debug("Smack teach in activated");
445 } catch (IOException e) {
446 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
447 "Smack packet could not be send: " + e.getMessage());
452 public void stopDiscovery() {
453 EnOceanTransceiver localTransceiver = transceiver;
454 if (localTransceiver != null) {
455 localTransceiver.stopDiscovery();
459 if (localTransceiver == null) {
460 throw new IOException("EnOceanTransceiver has state null");
462 localTransceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(false), null);
464 } catch (IOException e) {
465 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
466 "Smack packet could not be send: " + e.getMessage());
470 private void refreshProperties() {
471 if (getThing().getStatus() == ThingStatus.ONLINE && smackAvailable) {
472 logger.debug("request learned smack clients");
474 EnOceanTransceiver localTransceiver = transceiver;
475 if (localTransceiver != null) {
476 localTransceiver.sendBasePacket(ESP3PacketFactory.SA_RD_LEARNEDCLIENTS,
477 new ResponseListenerIgnoringTimeouts<RDLearnedClientsResponse>() {
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());
495 } catch (IOException e) {
496 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
497 "Smack packet could not be send: " + e.getMessage());
503 public void errorOccured(Throwable exception) {
504 EnOceanTransceiver localTransceiver = transceiver;
505 if (localTransceiver != null) {
506 localTransceiver.shutDown();
509 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, exception.getMessage());
512 public boolean sendTeachOuts() {
513 return sendTeachOuts;