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.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;
67 * The {@link EnOceanBridgeHandler} is responsible for sending ESP3Packages build by {@link EnOceanActuatorHandler} and
68 * transferring received ESP3Packages to {@link EnOceanSensorHandler}.
70 * @author Daniel Weber - Initial contribution
72 public class EnOceanBridgeHandler extends ConfigStatusBridgeHandler implements TransceiverErrorListener {
74 private Logger logger = LoggerFactory.getLogger(EnOceanBridgeHandler.class);
76 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
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
81 private byte[] baseId = null;
82 private Thing[] sendingThings = new Thing[128];
84 private SerialPortManager serialPortManager;
86 private boolean smackAvailable = false;
87 private boolean sendTeachOuts = true;
88 private Set<String> smackClients = Set.of();
90 public EnOceanBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) {
92 this.serialPortManager = serialPortManager;
96 public void handleCommand(ChannelUID channelUID, Command command) {
97 if (transceiver == null) {
98 updateStatus(ThingStatus.OFFLINE);
102 switch (channelUID.getId()) {
103 case CHANNEL_REPEATERMODE:
104 if (command instanceof RefreshType) {
105 sendMessage(ESP3PacketFactory.CO_RD_REPEATER,
106 new ResponseListenerIgnoringTimeouts<RDRepeaterResponse>() {
108 public void responseReceived(RDRepeaterResponse response) {
109 if (response.isValid() && response.isOK()) {
110 updateState(channelUID, response.getRepeaterLevel());
112 updateState(channelUID, new StringType(REPEATERMODE_OFF));
116 } else if (command instanceof StringType) {
117 sendMessage(ESP3PacketFactory.CO_WR_REPEATER((StringType) command),
118 new ResponseListenerIgnoringTimeouts<BaseResponse>() {
120 public void responseReceived(BaseResponse response) {
121 if (response.isOK()) {
122 updateState(channelUID, (StringType) command);
129 case CHANNEL_SETBASEID:
130 if (command instanceof StringType) {
132 byte[] id = HexUtils.hexToBytes(((StringType) command).toFullString());
134 sendMessage(ESP3PacketFactory.CO_WR_IDBASE(id),
135 new ResponseListenerIgnoringTimeouts<BaseResponse>() {
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"));
150 } catch (IllegalArgumentException e) {
151 updateState(channelUID, new StringType("BaseId could not be parsed"));
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");
168 if (connectorTask == null || connectorTask.isDone()) {
169 connectorTask = scheduler.scheduleWithFixedDelay(new Runnable() {
172 if (thing.getStatus() != ThingStatus.ONLINE) {
176 }, 0, 60, TimeUnit.SECONDS);
181 private synchronized void initTransceiver() {
183 EnOceanBridgeConfig c = getThing().getConfiguration().as(EnOceanBridgeConfig.class);
184 if (transceiver != null) {
185 transceiver.ShutDown();
188 switch (c.getESPVersion()) {
190 transceiver = new EnOceanESP2Transceiver(c.path, this, scheduler, serialPortManager);
191 smackAvailable = false;
192 sendTeachOuts = false;
195 transceiver = new EnOceanESP3Transceiver(c.path, this, scheduler, serialPortManager);
196 sendTeachOuts = c.sendTeachOuts;
202 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "opening serial port...");
203 transceiver.Initialize();
205 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "starting rx thread...");
206 transceiver.StartReceiving(scheduler);
207 logger.info("EnOceanSerialTransceiver RX thread up and running");
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");
217 baseId = new byte[4];
220 updateProperty(PROPERTY_BASE_ID, HexUtils.bytesToHex(baseId));
221 updateStatus(ThingStatus.ONLINE);
223 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
224 "trying to get bridge base id...");
226 logger.debug("request base id");
227 transceiver.sendBasePacket(ESP3PacketFactory.CO_RD_IDBASE,
228 new ResponseListenerIgnoringTimeouts<RDBaseIdResponse>() {
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);
239 updateStatus(ThingStatus.ONLINE);
241 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
242 "Could not get BaseId");
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>() {
253 public void responseReceived(BaseResponse response) {
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;
262 updateProperty("Postmaster mailboxes:", "Not supported");
263 smackAvailable = false;
270 logger.debug("request version info");
271 transceiver.sendBasePacket(ESP3PacketFactory.CO_RD_VERSION,
272 new ResponseListenerIgnoringTimeouts<RDVersionResponse>() {
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());
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");
294 public synchronized void dispose() {
295 if (transceiver != null) {
296 transceiver.ShutDown();
300 if (connectorTask != null && !connectorTask.isDone()) {
301 connectorTask.cancel(true);
302 connectorTask = null;
309 public Collection<ConfigStatusMessage> getConfigStatus() {
310 Collection<ConfigStatusMessage> configStatusMessages = new LinkedList<>();
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)
320 return configStatusMessages;
323 public byte[] getBaseId() {
324 return baseId.clone();
327 public boolean isSmackClient(Thing sender) {
328 return smackClients.contains(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId);
331 public Integer getNextSenderId(Thing sender) {
332 return getNextSenderId(sender.getConfiguration().as(EnOceanBaseConfig.class).enoceanId);
335 public Integer getNextSenderId(String enoceanId) {
336 EnOceanBridgeConfig config = getConfigAs(EnOceanBridgeConfig.class);
338 if (config.nextSenderId != null && sendingThings[config.nextSenderId] == null) {
339 Configuration c = this.editConfiguration();
340 c.put(PARAMETER_NEXT_SENDERID, null);
341 updateConfiguration(c);
343 return config.nextSenderId;
346 for (int i = 1; i < sendingThings.length; i++) {
347 if (sendingThings[i] == null || sendingThings[i].getConfiguration().as(EnOceanBaseConfig.class).enoceanId
348 .equalsIgnoreCase(enoceanId)) {
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);
361 public void addSender(int id, Thing thing) {
362 sendingThings[id] = thing;
365 public void removeSender(int id) {
366 sendingThings[id] = null;
369 public <T extends Response> void sendMessage(BasePacket message, ResponseListener<T> responseListener) {
371 transceiver.sendBasePacket(message, responseListener);
372 } catch (IOException e) {
373 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
377 public void addPacketListener(PacketListener listener) {
378 addPacketListener(listener, listener.getEnOceanIdToListenTo());
381 public void addPacketListener(PacketListener listener, long senderIdToListenTo) {
382 if (transceiver != null) {
383 transceiver.addPacketListener(listener, senderIdToListenTo);
387 public void removePacketListener(PacketListener listener) {
388 removePacketListener(listener, listener.getEnOceanIdToListenTo());
391 public void removePacketListener(PacketListener listener, long senderIdToListenTo) {
392 if (transceiver != null) {
393 transceiver.removePacketListener(listener, senderIdToListenTo);
397 public void startDiscovery(TeachInListener teachInListener) {
398 transceiver.startDiscovery(teachInListener);
400 if (smackAvailable) {
401 // activate smack teach in
402 logger.debug("activate smack teach in");
404 transceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(true),
405 new ResponseListenerIgnoringTimeouts<BaseResponse>() {
408 public void responseReceived(BaseResponse response) {
410 if (response.isOK()) {
411 logger.debug("Smack teach in activated");
415 } catch (IOException e) {
416 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
417 "Smack packet could not be send: " + e.getMessage());
422 public void stopDiscovery() {
423 transceiver.stopDiscovery();
426 transceiver.sendBasePacket(ESP3PacketFactory.SA_WR_LEARNMODE(false), null);
428 } catch (IOException e) {
429 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
430 "Smack packet could not be send: " + e.getMessage());
434 private void refreshProperties() {
435 if (getThing().getStatus() == ThingStatus.ONLINE && smackAvailable) {
437 logger.debug("request learned smack clients");
439 transceiver.sendBasePacket(ESP3PacketFactory.SA_RD_LEARNEDCLIENTS,
440 new ResponseListenerIgnoringTimeouts<RDLearnedClientsResponse>() {
443 public void responseReceived(RDLearnedClientsResponse response) {
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());
459 } catch (IOException e) {
460 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
461 "Smack packet could not be send: " + e.getMessage());
468 public void ErrorOccured(Throwable exception) {
469 transceiver.ShutDown();
471 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, exception.getMessage());
474 public boolean sendTeachOuts() {
475 return sendTeachOuts;