2 * Copyright (c) 2010-2021 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.phc.internal.handler;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
22 import java.util.TooManyListenersException;
23 import java.util.concurrent.BlockingQueue;
24 import java.util.concurrent.LinkedBlockingQueue;
25 import java.util.concurrent.ScheduledThreadPoolExecutor;
27 import org.apache.commons.lang3.StringUtils;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.phc.internal.PHCBindingConstants;
31 import org.openhab.binding.phc.internal.PHCHelper;
32 import org.openhab.core.io.transport.serial.PortInUseException;
33 import org.openhab.core.io.transport.serial.SerialPort;
34 import org.openhab.core.io.transport.serial.SerialPortEvent;
35 import org.openhab.core.io.transport.serial.SerialPortEventListener;
36 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
37 import org.openhab.core.io.transport.serial.SerialPortManager;
38 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.PercentType;
41 import org.openhab.core.library.types.StopMoveType;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.ThingUID;
48 import org.openhab.core.thing.binding.BaseBridgeHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.util.HexUtils;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * The {@link PHCBridgeHandler} is responsible for handling the serial Communication to and from the PHC Modules.
57 * @author Jonas Hohaus - Initial contribution
60 public class PHCBridgeHandler extends BaseBridgeHandler implements SerialPortEventListener {
62 private final Logger logger = LoggerFactory.getLogger(PHCBridgeHandler.class);
64 private static final int BAUD = 19200;
65 private static final int SEND_RETRY_COUNT = 20; // max count to send the same message
66 private static final int SEND_RETRY_TIME_MILLIS = 60; // time to wait for an acknowledge before send the message
67 // again in milliseconds
69 private @Nullable InputStream serialIn;
70 private @Nullable OutputStream serialOut;
71 private @Nullable SerialPort commPort;
72 private final SerialPortManager serialPortManager;
74 private final Map<Byte, Boolean> toggleMap = new HashMap<>();
75 private final InternalBuffer buffer = new InternalBuffer();
76 private final BlockingQueue<QueueObject> receiveQueue = new LinkedBlockingQueue<>();
77 private final BlockingQueue<QueueObject> sendQueue = new LinkedBlockingQueue<>();
78 private final ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(3);
80 private final byte emLedOutputState[] = new byte[32];
81 private final byte amOutputState[] = new byte[32];
82 private final byte dmOutputState[] = new byte[32];
84 private final List<Byte> modules = new ArrayList<>();
86 public PHCBridgeHandler(Bridge phcBridge, SerialPortManager serialPortManager) {
88 this.serialPortManager = serialPortManager;
92 public void initialize() {
93 String port = ((String) getConfig().get(PHCBindingConstants.PORT));
95 // find the given port
96 SerialPortIdentifier portId = serialPortManager.getIdentifier(port);
99 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
100 "Serial port '" + port + "' could not be found.");
105 // initialize serial port
106 SerialPort serialPort = portId.open(this.getClass().getName(), 2000); // owner, timeout
107 serialIn = serialPort.getInputStream();
108 // set port parameters
109 serialPort.setSerialPortParams(BAUD, SerialPort.DATABITS_8, SerialPort.STOPBITS_2, SerialPort.PARITY_NONE);
110 serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
112 serialPort.addEventListener(this);
113 // activate the DATA_AVAILABLE notifier
114 serialPort.notifyOnDataAvailable(true);
116 // get the output stream
117 serialOut = serialPort.getOutputStream();
119 commPort = serialPort;
124 for (int j = 0; j <= 0x1F; j++) {
125 serialWrite(buildMessage((byte) j, 0, b, false));
127 updateStatus(ThingStatus.ONLINE);
130 threadPoolExecutor.execute(new Runnable() {
134 processReceivedBytes();
138 // process received messages
139 threadPoolExecutor.execute(new Runnable() {
143 processReceiveQueue();
147 // sendig commands to the modules
148 threadPoolExecutor.execute(new Runnable() {
155 } catch (PortInUseException | TooManyListenersException e) {
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
157 "Could not open serial port " + port + ": " + e.getMessage());
158 } catch (UnsupportedCommOperationException e) {
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
160 "Could not configure serial port " + port + ": " + e.getMessage());
161 } catch (IOException e) {
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
163 "Failed to get input or output stream for serialPort: " + e.getMessage());
164 logger.debug("Failed to get inputstream for serialPort", e);
169 * Reads the data on serial port and puts it into the internal buffer.
172 public void serialEvent(SerialPortEvent event) {
173 if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE && serialIn != null) {
175 byte[] bytes = new byte[serialIn.available()];
176 serialIn.read(bytes);
180 if (logger.isTraceEnabled()) {
181 logger.trace("buffer offered {}", HexUtils.bytesToHex(bytes, " "));
183 } catch (IOException e) {
184 logger.warn("Error on reading input stream to internal buffer", e);
190 * process internal incoming buffer (recognize on read messages)
192 private void processReceivedBytes() {
193 int faultCounter = 0;
196 byte module = buffer.get();
199 // Recognition of messages from byte buffer.
200 // not a known module address
201 if (!modules.contains(module)) {
202 module = buffer.get();
206 if (logger.isDebugEnabled()) {
207 logger.debug("get module: {}", new String(HexUtils.byteToHex(module)));
210 byte sizeToggle = buffer.get();
212 // read length of command and check if makes sense
213 if ((sizeToggle < 1 || sizeToggle > 3) && ((sizeToggle & 0xFF) < 0x81 || (sizeToggle & 0xFF) > 0x83)) {
214 if (logger.isDebugEnabled()) {
215 logger.debug("get invalid sizeToggle: {}", new String(HexUtils.byteToHex(sizeToggle)));
222 // read toggle, size and command
223 int size = (sizeToggle & 0x7F);
224 boolean toggle = (sizeToggle & 0x80) == 0x80;
226 logger.debug("get toggle: {}", toggle);
228 byte[] command = new byte[size];
230 for (int i = 0; i < size; i++) {
231 command[i] = buffer.get();
235 if (logger.isTraceEnabled()) {
236 logger.trace("command read: {}", PHCHelper.bytesToBinaryString(command));
240 byte crcByte1 = buffer.get();
241 byte crcByte2 = buffer.get();
243 short crc = (short) (crcByte1 & 0xFF);
244 crc |= (crcByte2 << 8);
246 // calculate checkCrc
247 short checkCrc = calcCrc(module, sizeToggle, command);
250 if (crc != checkCrc) {
251 logger.debug("CRC not correct (crc from message, calculated crc): {}, {}", crc, checkCrc);
253 faultCounter = handleCrcFault(faultCounter);
255 module = buffer.get();
259 if (logger.isTraceEnabled()) {
260 logger.trace("get crc: {}", HexUtils.bytesToHex(new byte[] { crcByte1, crcByte2 }, " "));
265 processReceivedMsg(module, toggle, command);
266 module = buffer.get();
268 } catch (InterruptedException e) {
269 Thread.currentThread().interrupt();
273 private int handleCrcFault(int faultCounter) throws InterruptedException {
274 if (faultCounter > 0) {
275 // Normally in this case we read the message repeatedly offset to the real -> skip one to 6 bytes
276 for (int i = 0; i < faultCounter; i++) {
277 if (buffer.hasNext()) {
283 int resCounter = faultCounter + 1;
284 if (resCounter > 6) {
290 private void processReceivedMsg(byte module, boolean toggle, byte[] command) {
291 // Acknowledgement received (command first byte 0)
292 if (command[0] == 0) {
294 byte channel = 0; // only needed for dim
295 if ((module & 0xE0) == 0x40) {
296 moduleType = PHCBindingConstants.CHANNELS_AM;
297 } else if ((module & 0xE0) == 0xA0) {
298 moduleType = PHCBindingConstants.CHANNELS_DIM;
299 channel = (byte) ((command[0] >>> 5) & 0x0F);
301 moduleType = PHCBindingConstants.CHANNELS_EM_LED;
304 setModuleOutputState(moduleType, (byte) (module & 0x1F), command[1], channel);
305 toggleMap.put(module, !toggle);
307 // initialization (first byte FF)
308 } else if (command[0] == (byte) 0xFF) {
309 if ((module & 0xE0) == 0x00) { // EM
310 sendEmConfig(module);
311 } else if ((module & 0xE0) == 0x40 || (module & 0xE0) == 0xA0) { // AM, JRM and DIM
312 sendAmConfig(module);
315 logger.debug("initialization: {}", module);
317 // ignored - ping (first byte 01)
318 } else if (command[0] == 0x01) {
319 logger.debug("first byte 0x01 -> ignored");
321 // EM command / update
323 if (((module & 0xE0) == 0x00)) {
324 sendEmAcknowledge(module, toggle);
325 logger.debug("send acknowledge (modul, toggle) {} {}", module, toggle);
327 byte channel = (byte) ((command[0] >>> 4) & 0x0F);
329 OnOffType onOff = OnOffType.OFF;
331 if ((command[0] & 0x0F) == 2) {
332 onOff = OnOffType.ON;
335 QueueObject qo = new QueueObject(PHCBindingConstants.CHANNELS_EM, module, channel, onOff);
337 // put recognized message into queue
338 if (!receiveQueue.contains(qo)) {
339 receiveQueue.offer(qo);
342 // ignore if message not from EM module
343 } else if (logger.isDebugEnabled()) {
344 logger.debug("Incoming message (module, toggle, command) not from EM module: {} {} {}",
345 new String(HexUtils.byteToHex(module)), toggle, PHCHelper.bytesToBinaryString(command));
351 * process receive queue
353 private void processReceiveQueue() {
356 QueueObject qo = receiveQueue.take();
358 logger.debug("Consume Receive QueueObject: {}", qo);
359 handleIncomingCommand(qo.getModuleAddress(), qo.getChannel(), (OnOffType) qo.getCommand());
360 } catch (InterruptedException e) {
361 Thread.currentThread().interrupt();
369 private void processSendQueue() {
372 QueueObject qo = sendQueue.take();
375 } catch (InterruptedException e1) {
376 Thread.currentThread().interrupt();
381 private void sendQueueObject(QueueObject qo) {
383 // Send the command to the module until a response is received. Max. SEND_RETRY_COUNT repeats.
385 switch (qo.getModuleType()) {
386 case PHCBindingConstants.CHANNELS_AM:
387 sendAm(qo.getModuleAddress(), qo.getChannel(), qo.getCommand());
389 case PHCBindingConstants.CHANNELS_EM_LED:
390 sendEm(qo.getModuleAddress(), qo.getChannel(), qo.getCommand());
392 case PHCBindingConstants.CHANNELS_JRM:
393 sendJrm(qo.getModuleAddress(), qo.getChannel(), qo.getCommand(), qo.getTime());
395 case PHCBindingConstants.CHANNELS_DIM:
396 sendDim(qo.getModuleAddress(), qo.getChannel(), qo.getCommand(), qo.getTime());
402 Thread.sleep(SEND_RETRY_TIME_MILLIS);
403 } catch (InterruptedException e) {
404 Thread.currentThread().interrupt();
406 } while (!isChannelOutputState(qo.getModuleType(), qo.getModuleAddress(), qo.getChannel(), qo.getCommand())
407 && sendCount < SEND_RETRY_COUNT);
409 if (PHCBindingConstants.CHANNELS_JRM.equals(qo.getModuleType())) {
410 // there aren't state per channel for JRM modules
411 amOutputState[qo.getModuleAddress() & 0x1F] = -1;
412 } else if (PHCBindingConstants.CHANNELS_DIM.equals(qo.getModuleType())) {
413 // state ist the same for every dim level except zero/off -> inizialize state
414 // with 0x0F after sending an command.
415 dmOutputState[qo.getModuleAddress() & 0x1F] |= (0x0F << (qo.getChannel() * 4));
418 if (sendCount >= SEND_RETRY_COUNT) {
419 // change the toggle: if no acknowledge received it may be wrong.
420 byte module = qo.getModuleAddress();
421 if (PHCBindingConstants.CHANNELS_AM.equals(qo.getModuleType())
422 || PHCBindingConstants.CHANNELS_JRM.equals(qo.getModuleType())) {
424 } else if (PHCBindingConstants.CHANNELS_DIM.equals(qo.getModuleType())) {
427 toggleMap.put(module, !getToggle(module));
429 if (logger.isDebugEnabled()) {
430 logger.debug("No acknowledge from the module {} received.", qo.getModuleAddress());
435 private void setModuleOutputState(String moduleType, byte moduleAddress, byte state, byte channel) {
436 if (PHCBindingConstants.CHANNELS_EM_LED.equals(moduleType)) {
437 emLedOutputState[moduleAddress] = state;
438 } else if (PHCBindingConstants.CHANNELS_AM.equals(moduleType)) {
439 amOutputState[moduleAddress & 0x1F] = state;
440 } else if (PHCBindingConstants.CHANNELS_DIM.equals(moduleType)) {
441 dmOutputState[moduleAddress & 0x1F] = (byte) (state << channel * 4);
445 private boolean isChannelOutputState(String moduleType, byte moduleAddress, byte channel, Command cmd) {
446 int state = OnOffType.OFF.equals(cmd) ? 0 : 1;
448 if (PHCBindingConstants.CHANNELS_EM_LED.equals(moduleType)) {
449 return ((emLedOutputState[moduleAddress & 0x1F] >>> channel) & 0x01) == state;
450 } else if (PHCBindingConstants.CHANNELS_AM.equals(moduleType)) {
451 return ((amOutputState[moduleAddress & 0x1F] >>> channel) & 0x01) == state;
452 } else if (PHCBindingConstants.CHANNELS_JRM.equals(moduleType)) {
453 return (amOutputState[moduleAddress & 0x1F] != -1);
454 } else if (PHCBindingConstants.CHANNELS_DIM.equals(moduleType)) {
455 return ((dmOutputState[moduleAddress & 0x1F] >>> channel * 4) & 0x0F) != 0x0F;
461 private boolean getToggle(byte moduleAddress) {
462 if (!toggleMap.containsKey(moduleAddress)) {
463 toggleMap.put(moduleAddress, false);
466 return toggleMap.get(moduleAddress);
470 * Put the given command into the queue to send.
473 * @param moduleAddress
478 public void send(@Nullable String moduleType, int moduleAddress, String channel, Command command,
480 if (PHCBindingConstants.CHANNELS_JRM.equals(moduleType)
481 || PHCBindingConstants.CHANNELS_DIM.equals(moduleType)) {
482 sendQueue.offer(new QueueObject(moduleType, moduleAddress, channel, command, upDownTime));
484 sendQueue.offer(new QueueObject(moduleType, moduleAddress, channel, command));
488 private void sendAm(byte moduleAddress, byte channel, Command command) {
489 byte module = (byte) (moduleAddress | 0x40);
491 byte[] cmd = { (byte) (channel << 5) };
493 if (OnOffType.ON.equals(command)) {
498 serialWrite(buildMessage(module, channel, cmd, getToggle(module)));
501 private void sendEm(byte moduleAddress, byte channel, Command command) {
502 byte[] cmd = { (byte) (channel << 4) };
504 if (OnOffType.ON.equals(command)) {
509 serialWrite(buildMessage(moduleAddress, channel, cmd, getToggle(moduleAddress)));
512 private void sendJrm(byte moduleAddress, byte channel, Command command, short upDownTime) {
513 // The up and the down message needs two additional bytes for the time.
514 int size = (command == StopMoveType.STOP) ? 2 : 4;
515 byte[] cmd = new byte[size];
520 byte module = (byte) (moduleAddress | 0x40);
522 cmd[0] = (byte) (channel << 5);
525 switch (command.toString()) {
528 cmd[2] = (byte) (upDownTime & 0xFF);// Time 1/10 sec. LSB
529 cmd[3] = (byte) ((upDownTime >> 8) & 0xFF); // 1/10 sec. MSB
533 cmd[2] = (byte) (upDownTime & 0xFF);// Time 1/10 sec. LSB
534 cmd[3] = (byte) ((upDownTime >> 8) & 0xFF); // 1/10 sec. MSB
541 serialWrite(buildMessage(module, channel, cmd, getToggle(module)));
544 private void sendDim(byte moduleAddress, byte channel, Command command, short dimTime) {
545 byte module = (byte) (moduleAddress | 0xA0);
546 byte[] cmd = new byte[(command instanceof PercentType && !(((PercentType) command).byteValue() == 0)) ? 3 : 1];
548 cmd[0] = (byte) (channel << 5);
550 if (command instanceof OnOffType) {
551 if (OnOffType.ON.equals(command)) {
553 } else if (OnOffType.OFF.equals(command)) {
557 if (((PercentType) command).byteValue() == 0) {
561 cmd[1] = (byte) (((PercentType) command).byteValue() * 2.55);
562 cmd[2] = (byte) dimTime;
565 serialWrite(buildMessage(module, channel, cmd, getToggle(module)));
568 private void sendPorBroadcast() {
569 byte[] msg = buildMessage((byte) 0xFF, 0, new byte[] { 0 }, false);
570 for (int i = 0; i < 20; i++) {
576 private void sendAmConfig(byte moduleAddress) {
577 byte[] cmd = new byte[3];
579 cmd[0] = (byte) 0xFE;
581 cmd[2] = (byte) 0xFF;
583 serialWrite(buildMessage(moduleAddress, 0, cmd, false));
586 private void sendEmConfig(byte moduleAddress) {
587 byte[] cmd = new byte[52];
590 cmd[pos++] = (byte) 0xFE;
591 cmd[pos++] = (byte) 0x00; // POR
596 for (int i = 0; i < 16; i++) { // 16 inputs
597 cmd[pos++] = (byte) ((i << 4) | 0x02);
598 cmd[pos++] = (byte) ((i << 4) | 0x03);
599 cmd[pos++] = (byte) ((i << 4) | 0x05);
602 serialWrite(buildMessage(moduleAddress, 0, cmd, false));
605 private void sendEmAcknowledge(byte module, boolean toggle) {
606 byte[] msg = buildMessage(module, 0, new byte[] { 0 }, toggle);
607 for (int i = 0; i < 3; i++) { // send three times stops the module faster from sending messages if the first
608 // response is not recognized.
614 * Build a serial message from the given parameters.
622 private byte[] buildMessage(byte modulAddr, int channel, byte[] cmd, boolean toggle) {
623 int len = cmd.length;
624 byte[] buffer = new byte[len + 4];
626 buffer[0] = modulAddr;
627 buffer[1] = (byte) (toggle ? (len | 0x80) : len); // 0x80: 1000 0000
629 System.arraycopy(cmd, 0, buffer, 2, len);
631 short crc = calcCrc(modulAddr, buffer[1], cmd);
633 buffer[2 + len] = (byte) (crc & 0xFF);
634 buffer[3 + len] = (byte) ((crc >> 8) & 0xFF);
640 * Calculate the 16 bit crc of the message.
647 private short calcCrc(byte module, byte sizeToggle, byte[] cmd) {
648 short crc = (short) 0xFFFF;
650 crc = crc16Update(crc, module);
651 crc = crc16Update(crc, sizeToggle);
654 crc = crc16Update(crc, b);
662 * Update the 16 bit crc of the message.
668 private short crc16Update(short crc, byte messagePart) {
669 byte data = (byte) (messagePart ^ (crc & 0xFF));
673 return (short) (((data16 << 8) | (((crc >> 8) & 0xFF) & 0xFF)) ^ ((data >> 4) & 0xF)
674 ^ ((data16 << 3) & 0b11111111111));
678 * Send the incoming command to the appropriate handler and channel.
680 * @param moduleAddress
685 private void handleIncomingCommand(byte moduleAddress, int channel, OnOffType onOff) {
686 ThingUID uid = PHCHelper.getThingUIDreverse(PHCBindingConstants.THING_TYPE_EM, moduleAddress);
687 Thing thing = getThing().getThing(uid);
688 String channelId = "em#" + StringUtils.leftPad(Integer.toString(channel), 2, '0');
690 if (thing != null && thing.getHandler() != null) {
691 logger.debug("Input: {}, {}, {}", thing.getUID(), channelId, onOff);
693 PHCHandler handler = (PHCHandler) thing.getHandler();
694 if (handler != null) {
695 handler.handleIncoming(channelId, onOff);
697 logger.debug("No Handler for Thing {} available.", thing.getUID());
701 logger.debug("No Thing with UID {} available.", uid.getAsString());
705 private void serialWrite(byte[] msg) {
706 if (serialOut != null) {
708 // write to serial port
709 serialOut.write(msg);
711 } catch (IOException e) {
712 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
713 "Error writing '" + msg + "' to serial port : " + e.getMessage());
716 if (logger.isTraceEnabled()) {
717 logger.trace("send: {}", PHCHelper.bytesToBinaryString(msg));
723 * Adds the given address to the module list.
727 public void addModule(byte module) {
732 public void handleCommand(ChannelUID channelUID, Command command) {
737 public void dispose() {
738 threadPoolExecutor.shutdownNow();
739 if (commPort != null) {