diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 26240a2e68d5e..293f008b2a743 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1521,6 +1521,11 @@
org.openhab.binding.smhi
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.smsmodem
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.sncf
diff --git a/bundles/org.openhab.binding.smsmodem/NOTICE b/bundles/org.openhab.binding.smsmodem/NOTICE
new file mode 100644
index 0000000000000..b88b5d5c82640
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/NOTICE
@@ -0,0 +1,24 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
+
+== Third-party Content
+This binding includes code (under the org.smslib package) from the SMSlib project
+The code have been slighly modified to use the serial library provided by the openHAB runtime (amongst small other fixes, and code compliance for openHAB)
+
+smslib
+* license Apache 2.0 License
+* https://github.com/tdelenikas/smslib
+
+PduUtils Library
+* license Apache 2.0 License
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.smsmodem/README.md b/bundles/org.openhab.binding.smsmodem/README.md
new file mode 100644
index 0000000000000..84e3fa6db22dd
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/README.md
@@ -0,0 +1,146 @@
+# SMSModem Binding
+
+This binding connects to a USB serial GSM modem (or a network exposed one, see ser2net) and allows openHAB to send and receive SMS through it.
+
+Serial modem should all use the same communication protocol (AT message) and therefore this binding _should_ be compatible with every dongle.
+However, there is a gap between theory and reality and success may vary.
+The protocol stack is based on the no longer supported smslib project (more precisely a v4 fork), and all modems supported by this library should be OK.
+
+The following devices have been reported functional :
+
+- Huawei E180
+
+## Supported Things
+
+Two things are supported by this binding :
+
+- A *smsmodembridge*, representing the dongle connected on the local computer
+- A *smsmodemremotebridge*, representing the dongle exposed over the network (with ser2net or other similar software)
+- A *smsconversation*, representing a conversation between one distant msisdn and the msisdn on the sim card in the dongle.
+
+## Discovery
+
+There is no discovery process for *smsmodembridge* or *smsmodemremotebridge* thing.
+A *smsconversation* thing will be discovered and added to the inbox everytime the modem should receive a SMS by a new sender.
+
+## Thing Configuration
+
+The *smsmodembridge* or *smsmodemremotebridge* things requires at least two parameters to work properly.
+
+For local *smsmodembridge*:
+
+| Parameter Name | type | direct serial modem |
+|----------------|-------|----------------------|
+|serialPort| text | The serial port to access (eg. /dev/tty/USBx) |
+|baud| integer | Baud rate |
+
+For remote *smsmodemremotebridge*:
+
+| Parameter Name | type | serial over network |
+|----------------|-------|----------------------|
+|ip| text | IP address of the computer hosting the ser2net service|
+|networkPort| integer | The network port of the ser2net service |
+
+
+The other parameters are optional :
+
+| Parameter Name | type | description |
+|-----------------|------|---------------------|
+|simPin | text | If your sim card is protected, fill this field with the PIN code|
+|pollingInterval| integer | Delay between two checks for new message (in seconds)|
+|delayBetweenSend| integer | Delay to wait between two messages post (in milliseconds, could be necessary for slow modem)|
+
+The *smsconversation* thing is just a shortcut to address/receive messages with a specific msisdn. It is not mandatory to use the binding, as you can use action and trigger channel to send/receive a message once the smsmodem bridge is configured.
+
+| Parameter Name | type | description |
+|------------|----------|----------|
+| recipient | text | The msisdn of the phone you want to discuss with.|
+| deliveryReport | boolean | If enabled, ask the network for a delivery report (default false)|
+| encoding | text | The encoding to use when sending the message (either Enc7, Enc8, EncUcs2, EncCustom, default is Enc7). EncUcs2 is good for non latin character, but SMS character size limit is then reduced|
+
+
+
+## Channels
+
+The *smsconversation* supports the following channels :
+| channel | type | description |
+|----------|--------|------------------------------|
+| receive | String| The last message received |
+| send | String| A message to send |
+|deliverystatus| String| Delivery status (either UNKNOWN, QUEUED, SENT, PENDING, DELIVERED, EXPIRED, or FAILED). Several status are only possible if the delivery report parameter is enabled|
+
+## Trigger channels
+
+The *smsmodembridge* and *smsmodemremotebridge* has the following trigger channel :
+| Channel ID | event |
+|---------------------|----------------------------|
+|receivetrigger| The msisdn and message received (concatened with the '\|' character as a separator)|
+
+
+## Rule action
+
+This binding includes a rule action to send SMS.
+
+```
+(Rule DSL)
+val smsAction = getActions("smsmodem","smsmodem:smsmodembridge:")
+```
+
+```
+(javascript JSR)
+var smsAction = actions.get("smsmodem","smsmodem:smsmodembridge:");
+```
+
+Where uid is the Bridge UID of the *smsmodembridge* thing.
+
+Once this action instance is retrieved, you can invoke the 'send' method on it:
+
+```
+smsAction.sendSMS("1234567890", "Hello world!")
+```
+
+Or with a special encoding:
+
+```
+smsAction.sendSMS("1234567890", "Hello world!", "EncUcs2")
+```
+
+## Full Example
+
+### Thing configuration
+
+things/smsmodem.things:
+
+```
+Bridge smsmodem:smsmodembridge:adonglename "USB 3G Dongle " [ serialPort="/dev/ttyUSB0", baud="19200" ] {
+ Thing smsconversation aconversationname [ recipient="XXXXXXXXXXX", deliveryReport="true" ]
+}
+```
+
+### Send SMS
+
+`sms.rules` for DSL:
+
+```java
+rule "Alarm by SMS"
+when
+ Item Alarm changed
+then
+ val smsAction = getActions("smsmodem","smsmodem:smsmodembridge:dongleuid")
+ smsAction.sendSMS("33123456789", "Alert !")
+end
+```
+
+### Receive and forward SMS
+
+`sms.py` with the python helper library :
+
+```python
+@rule("smscommand.receive", description="Receive SMS and resend it")
+@when("Channel smsmodem:smsmodembridge:dongleuid:receivetrigger triggered")
+def smscommand(event):
+ sender_and_message = event.event.split("|")
+ sender = sender_and_message[0]
+ content = sender_and_message[1]
+ actions.get("smsmodem", "smsmodem:smsmodembridge:dongleuid").send("336123456789", sender + " just send the following message: " + content)
+```
diff --git a/bundles/org.openhab.binding.smsmodem/pom.xml b/bundles/org.openhab.binding.smsmodem/pom.xml
new file mode 100644
index 0000000000000..2ec2ae65295fd
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/pom.xml
@@ -0,0 +1,39 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.4.0-SNAPSHOT
+
+
+ org.openhab.binding.smsmodem
+
+ openHAB Add-ons :: Bundles :: SMSModem Binding
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+
+ add-source
+
+ generate-sources
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/Capabilities.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/Capabilities.java
new file mode 100644
index 0000000000000..824f34d915d0e
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/Capabilities.java
@@ -0,0 +1,49 @@
+package org.smslib;
+
+import java.util.BitSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class Capabilities {
+ BitSet caps = new BitSet();
+
+ public enum Caps {
+ CanSendMessage,
+ CanSendBinaryMessage,
+ CanSendUnicodeMessage,
+ CanSendWapMessage,
+ CanSendFlashMessage,
+ CanSendPortInfo,
+ CanSetSenderId,
+ CanSplitMessages,
+ CanRequestDeliveryStatus,
+ CanQueryDeliveryStatus,
+ CanQueryCreditBalance,
+ CanQueryCoverage,
+ CanSetValidityPeriod
+ }
+
+ public void set(Caps c) {
+ this.caps.set(c.ordinal());
+ }
+
+ public BitSet getCapabilities() {
+ return (BitSet) this.caps.clone();
+ }
+
+ @Override
+ public String toString() {
+ BitSet bs = (BitSet) getCapabilities().clone();
+ StringBuffer b = new StringBuffer();
+ for (Caps c : Caps.values()) {
+ b.append(String.format("%-30s : ", c.toString()));
+ b.append(bs.get(c.ordinal()) ? "YES" : "NO");
+ b.append("\n");
+ }
+ return b.toString();
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/CommunicationException.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/CommunicationException.java
new file mode 100644
index 0000000000000..3ba6702b90e2a
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/CommunicationException.java
@@ -0,0 +1,20 @@
+package org.smslib;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Wrapper for communication exception
+ */
+@NonNullByDefault
+public class CommunicationException extends Exception {
+
+ private static final long serialVersionUID = -5175636461754717860L;
+
+ public CommunicationException(String message, Exception cause) {
+ super(message, cause);
+ }
+
+ public CommunicationException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/DeviceInformation.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/DeviceInformation.java
new file mode 100644
index 0000000000000..07f551afd7eac
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/DeviceInformation.java
@@ -0,0 +1,161 @@
+package org.smslib;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.smslib.callback.IDeviceInformationListener;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class DeviceInformation {
+
+ @Nullable
+ private IDeviceInformationListener deviceInformationListener;
+
+ public enum Modes {
+ PDU,
+ TEXT
+ }
+
+ String manufacturer = "N/A";
+ String model = "N/A";
+ String swVersion = "N/A";
+ String serialNo = "N/A";
+ String imsi = "N/A";
+ int rssi = 0;
+
+ @Nullable
+ Modes mode;
+
+ int totalSent = 0;
+ int totalFailed = 0;
+ int totalReceived = 0;
+ int totalFailures = 0;
+
+ public void setDeviceInformationListener(@Nullable IDeviceInformationListener deviceInformationListener) {
+ this.deviceInformationListener = deviceInformationListener;
+ }
+
+ public synchronized void increaseTotalSent() {
+ this.totalSent++;
+ IDeviceInformationListener dil = deviceInformationListener;
+ if (dil != null) {
+ dil.setTotalSent(Integer.toString(totalSent));
+ }
+ }
+
+ public synchronized void increaseTotalFailed() {
+ this.totalFailed++;
+ IDeviceInformationListener dil = deviceInformationListener;
+ if (dil != null) {
+ dil.setTotalFailed(Integer.toString(totalFailed));
+ }
+ }
+
+ public synchronized void increaseTotalReceived() {
+ this.totalReceived++;
+ IDeviceInformationListener dil = deviceInformationListener;
+ if (dil != null) {
+ dil.setTotalReceived(Integer.toString(totalReceived));
+ }
+ }
+
+ public synchronized void increaseTotalFailures() {
+ this.totalFailures++;
+ IDeviceInformationListener dil = deviceInformationListener;
+ if (dil != null) {
+ dil.setTotalFailures(Integer.toString(totalFailures));
+ }
+ }
+
+ public String getManufacturer() {
+ return this.manufacturer;
+ }
+
+ public void setManufacturer(String manufacturer) {
+ this.manufacturer = manufacturer;
+ IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
+ if (finalDeviceInformationListener != null) {
+ finalDeviceInformationListener.setManufacturer(manufacturer);
+ }
+ }
+
+ public String getModel() {
+ return this.model;
+ }
+
+ public void setModel(String model) {
+ this.model = model;
+ IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
+ if (finalDeviceInformationListener != null) {
+ finalDeviceInformationListener.setModel(model);
+ }
+ }
+
+ public String getSwVersion() {
+ return this.swVersion;
+ }
+
+ public void setSwVersion(String swVersion) {
+ this.swVersion = swVersion;
+ IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
+ if (finalDeviceInformationListener != null) {
+ finalDeviceInformationListener.setSwVersion(swVersion);
+ }
+ }
+
+ public String getSerialNo() {
+ return this.serialNo;
+ }
+
+ public void setSerialNo(String serialNo) {
+ this.serialNo = serialNo;
+ IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
+ if (finalDeviceInformationListener != null) {
+ finalDeviceInformationListener.setSerialNo(serialNo);
+ }
+ }
+
+ public String getImsi() {
+ return this.imsi;
+ }
+
+ public void setImsi(String imsi) {
+ this.imsi = imsi;
+ IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
+ if (finalDeviceInformationListener != null) {
+ finalDeviceInformationListener.setImsi(imsi);
+ }
+ }
+
+ public int getRssi() {
+ return this.rssi;
+ }
+
+ public void setRssi(int rssi) {
+ this.rssi = rssi;
+ IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
+ if (finalDeviceInformationListener != null) {
+ finalDeviceInformationListener.setRssi(Integer.toString(rssi));
+ }
+ }
+
+ public @Nullable Modes getMode() {
+ return this.mode;
+ }
+
+ public void setMode(Modes mode) {
+ this.mode = mode;
+ IDeviceInformationListener finalDeviceInformationListener = deviceInformationListener;
+ if (finalDeviceInformationListener != null) {
+ finalDeviceInformationListener.setMode(mode.toString());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("MANUF:%s, MODEL:%s, SERNO:%s, IMSI:%s, SW:%s, RSSI:%ddBm, MODE:%s", getManufacturer(),
+ getModel(), getSerialNo(), getImsi(), getSwVersion(), getRssi(), getMode());
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/MessageReader.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/MessageReader.java
new file mode 100644
index 0000000000000..ce83446c14c9a
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/MessageReader.java
@@ -0,0 +1,387 @@
+package org.smslib;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.smslib.DeviceInformation.Modes;
+import org.smslib.Modem.Status;
+import org.smslib.message.DeliveryReportMessage;
+import org.smslib.message.InboundBinaryMessage;
+import org.smslib.message.InboundMessage;
+import org.smslib.message.Payload;
+import org.smslib.pduUtils.gsm3040.Pdu;
+import org.smslib.pduUtils.gsm3040.PduParser;
+import org.smslib.pduUtils.gsm3040.PduUtils;
+import org.smslib.pduUtils.gsm3040.SmsDeliveryPdu;
+import org.smslib.pduUtils.gsm3040.SmsStatusReportPdu;
+
+/**
+ *
+ * Poll the modem to check for new received messages
+ * (sms or delivery report)
+ *
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class MessageReader extends Thread {
+ static Logger logger = LoggerFactory.getLogger(MessageReader.class);
+
+ Modem modem;
+
+ private static int HOURS_TO_RETAIN_ORPHANED_MESSAGE_PARTS = 72;
+
+ public MessageReader(Modem modem) {
+ this.modem = modem;
+ }
+
+ @Override
+ public void run() {
+ logger.debug("Started!");
+ if (this.modem.getStatus() == Status.Started) {
+ try {
+ this.modem.getModemDriver().lock();
+ ArrayList messageList = new ArrayList();
+ try {
+ for (int i = 0; i < (this.modem.getModemDriver().getMemoryLocations().length() / 2); i++) {
+ String memLocation = this.modem.getModemDriver().getMemoryLocations().substring((i * 2),
+ (i * 2) + 2);
+ String data = this.modem.getModemDriver().atGetMessages(memLocation).getResponseData();
+ if (data.length() > 0) {
+ messageList.addAll((this.modem.getDeviceInformation().getMode() == Modes.PDU
+ ? parsePDU(data, memLocation)
+ : parseTEXT(data, memLocation)));
+ }
+ }
+ } finally {
+ this.modem.getModemDriver().unlock();
+ }
+ for (InboundMessage message : messageList) {
+ processMessage(message);
+ }
+
+ } catch (CommunicationException | IOException e) {
+ logger.error("Unhandled exception while trying to read new messages", e);
+ modem.error();
+ }
+ }
+ logger.debug("Stopped!");
+ }
+
+ private ArrayList parsePDU(String data, String memLocation) throws IOException {
+ ArrayList messageList = new ArrayList<>();
+ List> mpMsgList = new ArrayList<>();
+ BufferedReader reader = new BufferedReader(new StringReader(data));
+ while (true) {
+ String line = reader.readLine();
+ if (line == null) {
+ break;
+ }
+ PduParser parser = new PduParser();
+ int i = line.indexOf(':');
+ int j = line.indexOf(',');
+ if (j == -1) {
+ logger.error("Bad PDU announce : {}", line);
+ continue;
+ }
+ int memIndex = Integer.parseInt(line.substring(i + 1, j).trim());
+ i = line.lastIndexOf(',');
+ j = line.length();
+ int pduSize = Integer.parseInt(line.substring(i + 1, j).trim());
+ String pduString = reader.readLine().trim();
+ if ((pduSize > 0) && ((pduSize * 2) == pduString.length())) {
+ pduString = "00" + pduString;
+ }
+ Pdu pdu = parser.parsePdu(pduString);
+ if (pdu instanceof SmsDeliveryPdu) {
+ logger.debug("PDU = {}", pdu.toString());
+ InboundMessage msg = null;
+ if (pdu.isBinary()) {
+ msg = new InboundBinaryMessage((SmsDeliveryPdu) pdu, memLocation, memIndex);
+ } else {
+ msg = new InboundMessage((SmsDeliveryPdu) pdu, memLocation, memIndex);
+ }
+ msg.setGatewayId(this.modem.getGatewayId());
+ msg.setGatewayId(this.modem.getGatewayId());
+ logger.debug("IN-DTLS: MI:{} REF:{} MAX:{} SEQ:{}", msg.getMemIndex(), msg.getMpRefNo(),
+ msg.getMpMaxNo(), msg.getMpSeqNo());
+ if (msg.getMpRefNo() == 0) {
+ messageList.add(msg);
+ } else {
+ // multi-part message
+ int k, l;
+ List tmpList;
+ InboundMessage listMsg;
+ boolean found, duplicate;
+ found = false;
+ for (k = 0; k < mpMsgList.size(); k++) {
+ // List of List
+ tmpList = mpMsgList.get(k);
+ listMsg = tmpList.get(0);
+ // check if current message list is for this message
+ if (listMsg.getMpRefNo() == msg.getMpRefNo()) {
+ duplicate = false;
+ // check if the message is already in the message list
+ for (l = 0; l < tmpList.size(); l++) {
+ listMsg = tmpList.get(l);
+ if (listMsg.getMpSeqNo() == msg.getMpSeqNo()) {
+ duplicate = true;
+ break;
+ }
+ }
+ if (!duplicate) {
+ tmpList.add(msg);
+ }
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // no existing list present for this message
+ // add one
+ tmpList = new ArrayList<>();
+ tmpList.add(msg);
+ mpMsgList.add(tmpList);
+ }
+ }
+ } else if (pdu instanceof SmsStatusReportPdu) {
+ DeliveryReportMessage msg;
+ msg = new DeliveryReportMessage((SmsStatusReportPdu) pdu, memLocation, memIndex);
+ msg.setGatewayId(this.modem.getGatewayId());
+ messageList.add(msg);
+ }
+ }
+ checkMpMsgList(messageList, mpMsgList);
+ List tmpList;
+ for (int k = 0; k < mpMsgList.size(); k++) {
+ tmpList = mpMsgList.get(k);
+ tmpList.clear();
+ }
+ mpMsgList.clear();
+ return messageList;
+ }
+
+ private ArrayList parseTEXT(String data, String memLocation) throws IOException {
+ ArrayList messageList = new ArrayList<>();
+ BufferedReader reader;
+ String line;
+ Calendar cal1 = Calendar.getInstance();
+ Calendar cal2 = Calendar.getInstance();
+ String myData = data;
+ myData = myData.replaceAll("\\s+OK\\s+", "\nOK");
+ myData = myData.replaceAll("$", "\n");
+ logger.debug(myData);
+ reader = new BufferedReader(new StringReader(myData));
+ for (;;) {
+ line = reader.readLine();
+ if (line == null) {
+ break;
+ }
+ line = line.trim();
+ if (line.length() > 0) {
+ break;
+ }
+ }
+ while (true) {
+ if (line == null) {
+ break;
+ }
+ if (line.length() <= 0 || "OK".equalsIgnoreCase(line)) {
+ break;
+ }
+ int i = line.indexOf(':');
+ int j = line.indexOf(',');
+ int memIndex = Integer.parseInt(line.substring(i + 1, j).trim());
+ StringTokenizer tokens = new StringTokenizer(line, ",");
+ tokens.nextToken();
+ tokens.nextToken();
+ String tmpLine = "";
+ if (Character.isDigit(tokens.nextToken().trim().charAt(0))) {
+ line = line.replaceAll(",,", ", ,");
+ tokens = new StringTokenizer(line, ",");
+ tokens.nextToken();
+ tokens.nextToken();
+ tokens.nextToken();
+ String messageId = tokens.nextToken();
+ String recipient = tokens.nextToken().replaceAll("\"", "");
+ String dateStr = tokens.nextToken().replaceAll("\"", "");
+ if (dateStr.indexOf('/') == -1) {
+ dateStr = tokens.nextToken().replaceAll("\"", "");
+ }
+ cal1.set(Calendar.YEAR, 2000 + Integer.parseInt(dateStr.substring(0, 2)));
+ cal1.set(Calendar.MONTH, Integer.parseInt(dateStr.substring(3, 5)) - 1);
+ cal1.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStr.substring(6, 8)));
+ dateStr = tokens.nextToken().replaceAll("\"", "");
+ cal1.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateStr.substring(0, 2)));
+ cal1.set(Calendar.MINUTE, Integer.parseInt(dateStr.substring(3, 5)));
+ cal1.set(Calendar.SECOND, Integer.parseInt(dateStr.substring(6, 8)));
+ dateStr = tokens.nextToken().replaceAll("\"", "");
+ cal2.set(Calendar.YEAR, 2000 + Integer.parseInt(dateStr.substring(0, 2)));
+ cal2.set(Calendar.MONTH, Integer.parseInt(dateStr.substring(3, 5)) - 1);
+ cal2.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStr.substring(6, 8)));
+ dateStr = tokens.nextToken().replaceAll("\"", "");
+ cal2.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateStr.substring(0, 2)));
+ cal2.set(Calendar.MINUTE, Integer.parseInt(dateStr.substring(3, 5)));
+ cal2.set(Calendar.SECOND, Integer.parseInt(dateStr.substring(6, 8)));
+ DeliveryReportMessage msg;
+ msg = new DeliveryReportMessage(messageId, recipient, memLocation, memIndex, cal1.getTime(),
+ cal2.getTime());
+ msg.setGatewayId(this.modem.getGatewayId());
+ messageList.add(msg);
+ } else {
+ line = line.replaceAll(",,", ", ,");
+ tokens = new StringTokenizer(line, ",");
+ tokens.nextToken();
+ tokens.nextToken();
+ String originator = tokens.nextToken().replaceAll("\"", "");
+ tokens.nextToken();
+ String dateStr = tokens.nextToken().replaceAll("\"", "");
+ cal1.set(Calendar.YEAR, 2000 + Integer.parseInt(dateStr.substring(0, 2)));
+ cal1.set(Calendar.MONTH, Integer.parseInt(dateStr.substring(3, 5)) - 1);
+ cal1.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStr.substring(6, 8)));
+ dateStr = tokens.nextToken().replaceAll("\"", "");
+ cal1.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateStr.substring(0, 2)));
+ cal1.set(Calendar.MINUTE, Integer.parseInt(dateStr.substring(3, 5)));
+ cal1.set(Calendar.SECOND, Integer.parseInt(dateStr.substring(6, 8)));
+ String msgText = "";
+ while (true) {
+ tmpLine = reader.readLine();
+ if (tmpLine == null) {
+ break;
+ }
+ if (tmpLine.startsWith("+CMGL")) {
+ break;
+ }
+ if (tmpLine.startsWith("+CMGR")) {
+ break;
+ }
+ msgText += (msgText.length() == 0 ? "" : "\n") + tmpLine;
+ }
+ InboundMessage msg = new InboundMessage(originator, msgText.trim(), cal1.getTime(), memLocation,
+ memIndex);
+ msg.setGatewayId(this.modem.getGatewayId());
+ messageList.add(msg);
+ }
+ while (true) {
+ // line = reader.readLine();
+ line = ((tmpLine == null || tmpLine.length() == 0) ? reader.readLine() : tmpLine);
+ if (line == null) {
+ break;
+ }
+ line = line.trim();
+ if (line.length() > 0) {
+ break;
+ }
+ }
+ }
+ reader.close();
+ return messageList;
+ }
+
+ private void checkMpMsgList(Collection msgList, List> mpMsgList) {
+ int k, l, m;
+ List tmpList;
+ InboundMessage listMsg, mpMsg;
+ boolean found;
+ mpMsg = null;
+ logger.debug("CheckMpMsgList(): MAINLIST: {}", mpMsgList.size());
+ for (k = 0; k < mpMsgList.size(); k++) {
+ tmpList = mpMsgList.get(k);
+ logger.debug("CheckMpMsgList(): SUBLIST[{}]: ", tmpList.size());
+ listMsg = tmpList.get(0);
+ found = false;
+ if (listMsg.getMpMaxNo() == tmpList.size()) {
+ found = true;
+ for (l = 0; l < tmpList.size(); l++) {
+ for (m = 0; m < tmpList.size(); m++) {
+ listMsg = tmpList.get(m);
+ if (listMsg.getMpSeqNo() == (l + 1)) {
+ if (listMsg.getMpSeqNo() == 1) {
+ mpMsg = listMsg;
+ mpMsg.setMpMemIndex(mpMsg.getMemIndex());
+ if (listMsg.getMpMaxNo() == 1) {
+ msgList.add(mpMsg);
+ }
+ } else {
+ if (mpMsg != null) {
+ String textToAdd = listMsg.getPayload().getText();
+ if (mpMsg.getEndsWithMultiChar()) {
+ if (textToAdd == null) {
+ throw new UnrecoverableSmslibException("Cannot add text to message");
+ }
+ // adjust first char of textToAdd
+ logger.debug("Adjusting dangling multi-char: {} --> {}", textToAdd.charAt(0),
+ PduUtils.getMultiCharFor(textToAdd.charAt(0)));
+ textToAdd = PduUtils.getMultiCharFor(textToAdd.charAt(0))
+ + textToAdd.substring(1);
+ }
+ mpMsg.setEndsWithMultiChar(listMsg.getEndsWithMultiChar());
+ mpMsg.setPayload(new Payload(mpMsg.getPayload().getText() + textToAdd));
+ // }
+ mpMsg.setMpSeqNo(listMsg.getMpSeqNo());
+ mpMsg.setMpMemIndex(listMsg.getMemIndex());
+ if (listMsg.getMpSeqNo() == listMsg.getMpMaxNo()) {
+ mpMsg.setMemIndex(-1);
+ msgList.add(mpMsg);
+ mpMsg = null;
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+ tmpList.clear();
+ tmpList = null;
+ }
+ if (found) {
+ mpMsgList.remove(k);
+ k--;
+ }
+ }
+ // Check the remaining parts for "orphaned" status
+ for (List remainingList : mpMsgList) {
+ for (InboundMessage msg : remainingList) {
+ Date sentDate = msg.getSentDate();
+ if (sentDate == null || getAgeInHours(sentDate) > HOURS_TO_RETAIN_ORPHANED_MESSAGE_PARTS) {
+ try {
+ this.modem.delete(msg);
+ } catch (CommunicationException e) {
+ logger.error("Could not delete orphaned message: {}", msg.toString(), e);
+ }
+ }
+ }
+ }
+ }
+
+ private static int getAgeInHours(Date fromDate) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(new java.util.Date());
+ long now = cal.getTimeInMillis();
+ cal.setTime(fromDate);
+ long past = cal.getTimeInMillis();
+ return (int) ((now - past) / (60 * 60 * 1000));
+ }
+
+ private void processMessage(InboundMessage message) {
+ String messageSignature = message.getSignature();
+ if (!this.modem.getReadMessagesSet().contains(messageSignature)) {
+ this.modem.getDeviceInformation().increaseTotalReceived();
+ if (message instanceof DeliveryReportMessage) {
+ modem.processDeliveryReport((DeliveryReportMessage) message);
+ } else {
+ modem.processMessage(message);
+ }
+ this.modem.getReadMessagesSet().add(messageSignature);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/MessageSender.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/MessageSender.java
new file mode 100644
index 0000000000000..35e1e7a46bcc0
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/MessageSender.java
@@ -0,0 +1,78 @@
+package org.smslib;
+
+import java.util.Queue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.smslib.message.OutboundMessage;
+import org.smslib.message.OutboundMessage.FailureCause;
+import org.smslib.message.OutboundMessage.SentStatus;
+
+/**
+ * Poll the modem queue and send messages
+ *
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class MessageSender extends Thread {
+ static Logger logger = LoggerFactory.getLogger(MessageSender.class);
+
+ Queue messageQueue;
+
+ Modem modem;
+
+ private int gatewayDispatcherYield;
+
+ private AtomicBoolean isRunning = new AtomicBoolean(false);
+ private boolean interrupt = false;
+
+ public MessageSender(String name, Queue messageQueue, Modem modem, int gatewayDispatcherYield) {
+ setName(name);
+ setDaemon(false);
+ this.messageQueue = messageQueue;
+ this.modem = modem;
+ this.gatewayDispatcherYield = gatewayDispatcherYield;
+ }
+
+ @Override
+ public void run() {
+ if (!isRunning.getAndSet(true)) {
+ interrupt = false; // reset interruption status
+ try {
+ logger.debug("Started!");
+ while (!interrupt && messageQueue.size() > 0) {
+ try {
+ OutboundMessage message = messageQueue.poll();
+ if (message != null) {
+ try {
+ this.modem.send(message);
+ } catch (CommunicationException e) {
+ logger.error("Send failed!", e);
+ message.setSentStatus(SentStatus.Failed);
+ message.setFailureCause(FailureCause.None);
+ } finally {
+ this.modem.processMessageSent(message);
+ sleep(this.gatewayDispatcherYield);
+ }
+ }
+ } catch (InterruptedException e) {
+ logger.debug("Message dispatcher thread interrupted", e);
+ }
+ }
+ logger.debug("Ended!");
+ } finally {
+ this.isRunning.set(false);
+ }
+ }
+ }
+
+ public void setInterrupt() {
+ this.interrupt = true;
+ }
+
+ public boolean isRunning() {
+ return isRunning.get();
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/Modem.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/Modem.java
new file mode 100644
index 0000000000000..a654bac990517
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/Modem.java
@@ -0,0 +1,444 @@
+package org.smslib;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Queue;
+import java.util.Random;
+import java.util.StringTokenizer;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.smslib.Capabilities.Caps;
+import org.smslib.DeviceInformation.Modes;
+import org.smslib.callback.IDeviceInformationListener;
+import org.smslib.callback.IInboundOutboundMessageListener;
+import org.smslib.callback.IModemStatusListener;
+import org.smslib.driver.AbstractModemDriver;
+import org.smslib.driver.IPModemDriver;
+import org.smslib.driver.JSerialModemDriver;
+import org.smslib.message.DeliveryReportMessage;
+import org.smslib.message.InboundMessage;
+import org.smslib.message.MsIsdn;
+import org.smslib.message.OutboundMessage;
+import org.smslib.message.OutboundMessage.FailureCause;
+import org.smslib.message.OutboundMessage.SentStatus;
+import org.smslib.message.Payload;
+import org.smslib.message.Payload.Type;
+
+/**
+ * The Modem class is an abstraction, central to all operations
+ *
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class Modem {
+ static Logger logger = LoggerFactory.getLogger(Modem.class);
+
+ public enum Status {
+ Starting,
+ Started,
+ Stopping,
+ Stopped,
+ Error
+ }
+
+ AbstractModemDriver modemDriver;
+
+ String simPin;
+ MsIsdn smscNumber;
+ protected String operatorId = "";
+ String gatewayId = "";
+ String description = "";
+
+ private ScheduledExecutorService scheduledService;
+ @Nullable
+ ScheduledFuture> messageReader;
+ MessageSender messageSender;
+ Queue messageQueue = new ConcurrentLinkedQueue<>();
+ HashSet readMessagesSet;
+
+ Status status = Status.Stopped;
+ Lock startAndStoplock = new ReentrantLock();
+
+ int multipartReferenceNo = 0;
+
+ Capabilities capabilities = new Capabilities();
+ DeviceInformation deviceInformation = new DeviceInformation();
+ private @Nullable IModemStatusListener modemStatusCallback = null;
+ private @Nullable IInboundOutboundMessageListener messageCallback = null;
+
+ private Random randomizer = new Random();
+
+ private AtomicBoolean isStopping = new AtomicBoolean(false);
+ private AtomicBoolean isStarting = new AtomicBoolean(false);
+
+ /**
+ * Time between sending messages (ms)
+ */
+ private int gatewayDispatcherYield = 100;
+
+ /**
+ * Time between polling for new messages (ms)
+ */
+ public int modemPollingInterval = 15;
+
+ public Modem(SerialPortManager serialPortManager, String address, int port, String simPin,
+ ScheduledExecutorService scheduledService, Integer pollingInterval, Integer delayBetweenSend) {
+ this.gatewayId = address + "-" + port;
+ this.scheduledService = scheduledService;
+ this.modemPollingInterval = pollingInterval;
+ this.gatewayDispatcherYield = delayBetweenSend;
+ setDescription("GSM Modem " + address + "/" + port);
+
+ Capabilities caps = new Capabilities();
+ caps.set(Caps.CanSendMessage);
+ caps.set(Caps.CanSendBinaryMessage);
+ caps.set(Caps.CanSendUnicodeMessage);
+ caps.set(Caps.CanSendWapMessage);
+ caps.set(Caps.CanSendFlashMessage);
+ caps.set(Caps.CanSendPortInfo);
+ caps.set(Caps.CanSplitMessages);
+ caps.set(Caps.CanRequestDeliveryStatus);
+ setCapabilities(caps);
+ if (isPortAnIpAddress(address)) {
+ this.modemDriver = new IPModemDriver(this, address, port);
+ } else {
+ this.modemDriver = new JSerialModemDriver(serialPortManager, this, address, port);
+ }
+ this.simPin = simPin;
+ this.smscNumber = new MsIsdn();
+ this.readMessagesSet = new HashSet<>();
+ this.messageSender = new MessageSender(String.format("Gateway Dispatcher 1 [%s]", this.gatewayId), messageQueue,
+ this, gatewayDispatcherYield);
+ }
+
+ final public boolean start() {
+ if (!isStarting.getAndSet(true)) {
+ this.startAndStoplock.lock();
+ try {
+ if ((getStatus() == Status.Stopped) || (getStatus() == Status.Error)) {
+ try {
+ setStatus(Status.Starting);
+ logger.debug("Starting gateway: {}", toShortString());
+ this.modemDriver.lock();
+ try {
+ this.modemDriver.openPort();
+ this.modemDriver.initializeModem();
+ ScheduledFuture> messageReaderFinal = this.messageReader;
+ if (messageReaderFinal != null) {
+ messageReaderFinal.cancel(true);
+ }
+ this.messageReader = scheduledService.scheduleWithFixedDelay(new MessageReader(this), 15,
+ modemPollingInterval, TimeUnit.SECONDS);
+ this.modemDriver.refreshRssi();
+ this.messageSender = new MessageSender(
+ String.format("Gateway Dispatcher 1 [%s]", this.gatewayId), messageQueue, this,
+ gatewayDispatcherYield);
+ startSendingQueue();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Gateway: {}: {}, SL:{}, SIG: {} / {}", toShortString(),
+ getDeviceInformation().toString(), this.modemDriver.getMemoryLocations(),
+ this.modemDriver.getSignature(true), this.modemDriver.getSignature(false));
+ }
+ } finally {
+ this.modemDriver.unlock();
+ }
+ setStatus(Status.Started);
+ } catch (CommunicationException e) {
+ logger.error("Communication exception when trying to start", e);
+ try {
+ stop();
+ } finally {
+ setStatus(Status.Error);
+ }
+ }
+ }
+ } finally {
+ this.startAndStoplock.unlock();
+ this.isStarting.set(false);
+ }
+ }
+ return (getStatus() == Status.Started);
+ }
+
+ final public boolean stop() {
+ if (!isStopping.getAndSet(true)) {
+ this.startAndStoplock.lock();
+ try {
+ if ((getStatus() == Status.Started) || (getStatus() == Status.Error)) {
+ setStatus(Status.Stopping);
+ logger.debug("Stopping gateway: {}", toShortString());
+ if (messageSender.isRunning()) {
+ this.messageSender.setInterrupt();
+ }
+ logger.warn("Gateway stopping, message not delivered : {}", this.messageQueue.size());
+ ScheduledFuture> messageReaderFinal = this.messageReader;
+ if (messageReaderFinal != null) {
+ messageReaderFinal.cancel(true);
+ }
+ this.modemDriver.lock();
+ try {
+ this.modemDriver.closePort();
+ } finally {
+ this.modemDriver.unlock();
+ }
+ setStatus(Status.Stopped);
+ }
+ } finally {
+ this.startAndStoplock.unlock();
+ isStopping.set(false);
+ }
+ }
+ return (getStatus() == Status.Stopped);
+ }
+
+ final public void error() {
+ this.stop();
+ this.status = Status.Error;
+ }
+
+ final public boolean send(OutboundMessage message) throws CommunicationException {
+ try {
+ if (getStatus() != Status.Started) {
+ logger.debug("Outbound message routed via non-started gateway: {} ({})", message.toShortString(),
+ getStatus());
+ return false;
+ }
+ this.modemDriver.lock();
+ try {
+ if (getDeviceInformation().getMode() == Modes.PDU) {
+ List pdus = message.getPdus(getSmscNumber(), getNextMultipartReferenceNo());
+ for (String pdu : pdus) {
+ int j = pdu.length() / 2 - 1;
+ int refNo = this.modemDriver.atSendPDUMessage(j, pdu);
+ if (refNo >= 0) {
+ message.setGatewayId(getGatewayId());
+ message.setSentDate(new Date());
+ message.getOperatorMessageIds().add(String.valueOf(refNo));
+ message.setSentStatus(SentStatus.Sent);
+ message.setFailureCause(FailureCause.None);
+ } else {
+ message.setSentStatus(SentStatus.Failed);
+ message.setFailureCause(FailureCause.GatewayFailure);
+ }
+ }
+ } else {
+ MsIsdn recipientAddress = message.getRecipientAddress();
+ Payload payload = message.getPayload();
+ if (recipientAddress == null) {
+ throw new IllegalArgumentException("Recipient is null");
+ }
+ String text = payload.getText();
+ if (payload.getType() == Type.Binary || text == null) {
+ throw new IllegalArgumentException("Cannot send sms in binary format");
+ }
+ int refNo = this.modemDriver.atSendTEXTMessage(recipientAddress.getAddress(), text);
+ if (refNo >= 0) {
+ message.setGatewayId(getGatewayId());
+ message.setSentDate(new Date());
+ message.getOperatorMessageIds().add(String.valueOf(refNo));
+ message.setSentStatus(SentStatus.Sent);
+ message.setFailureCause(FailureCause.None);
+ } else {
+ message.setSentStatus(SentStatus.Failed);
+ message.setFailureCause(FailureCause.GatewayFailure);
+ }
+ }
+ if (message.getSentStatus() == SentStatus.Sent) {
+ getDeviceInformation().increaseTotalSent();
+ } else {
+ getDeviceInformation().increaseTotalFailed();
+ }
+ } finally {
+ this.modemDriver.unlock();
+ }
+ return message.getSentStatus() == SentStatus.Sent;
+ } catch (CommunicationException e) {
+ getDeviceInformation().increaseTotalFailures();
+ throw e;
+ }
+ }
+
+ final public boolean delete(InboundMessage message) throws CommunicationException {
+ if (getStatus() != Status.Started) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Delete message via non-started gateway: {} ({})", message.toShortString(), getStatus());
+ }
+ return false;
+ }
+
+ this.modemDriver.lock();
+ try {
+ this.readMessagesSet.remove(message.getSignature());
+ if (message.getMemIndex() >= 0) {
+ return this.modemDriver.atDeleteMessage(message.getMemLocation(), message.getMemIndex()).isResponseOk();
+ }
+ if ((message.getMemIndex() == -1) && (message.getMpMemIndex().length() > 0)) {
+ StringTokenizer tokens = new StringTokenizer(message.getMpMemIndex(), ",");
+ while (tokens.hasMoreTokens()) {
+ this.modemDriver.atDeleteMessage(message.getMemLocation(), Integer.valueOf(tokens.nextToken()));
+ }
+ return true;
+ }
+ return false;
+ } finally {
+ this.modemDriver.unlock();
+ }
+ }
+
+ public boolean queue(OutboundMessage message) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Queue: {}", message.toShortString());
+ }
+ boolean added = messageQueue.add(message);
+ IInboundOutboundMessageListener messageCallbackFinal = messageCallback;
+ if (messageCallbackFinal != null) {
+ messageCallbackFinal.messageSent(message);
+ }
+ startSendingQueue();
+ return added;
+ }
+
+ private void startSendingQueue() {
+ if (messageQueue.size() > 0 && (!this.messageSender.isRunning())) {
+ this.scheduledService.execute(messageSender);
+ }
+ }
+
+ public DeviceInformation getDeviceInformation() {
+ return this.deviceInformation;
+ }
+
+ public AbstractModemDriver getModemDriver() {
+ return this.modemDriver;
+ }
+
+ public String getSimPin() {
+ return this.simPin;
+ }
+
+ public MsIsdn getSmscNumber() {
+ return this.smscNumber;
+ }
+
+ public void setSmscNumber(MsIsdn smscNumber) {
+ this.smscNumber = smscNumber;
+ }
+
+ public HashSet getReadMessagesSet() {
+ return this.readMessagesSet;
+ }
+
+ private void setStatus(Status status) {
+ Status oldStatus = this.status;
+ this.status = status;
+ Status newStatus = this.status;
+ IModemStatusListener modemStatusCallbackFinal = modemStatusCallback;
+ if (modemStatusCallbackFinal != null) {
+ modemStatusCallbackFinal.processStatusCallback(oldStatus, newStatus);
+ }
+ }
+
+ protected int getNextMultipartReferenceNo() {
+ if (this.multipartReferenceNo == 0) {
+ this.multipartReferenceNo = this.randomizer.nextInt();
+ if (this.multipartReferenceNo < 0) {
+ this.multipartReferenceNo *= -1;
+ }
+ this.multipartReferenceNo %= 65536;
+ }
+ this.multipartReferenceNo = (this.multipartReferenceNo + 1) % 65536;
+ return this.multipartReferenceNo;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer b = new StringBuffer(1024);
+ b.append("== GATEWAY ========================================================================%n");
+ b.append(String.format("Gateway ID: %s%n", getGatewayId()));
+ b.append(String.format("-- Capabilities --%n"));
+ b.append(capabilities.toString());
+ b.append(String.format("-- Settings --%n"));
+ b.append("== GATEWAY END ========================================================================%n");
+ return b.toString();
+ }
+
+ public String toShortString() {
+ return getGatewayId() + String.format(" [%s]", this.modemDriver.getPortInfo());
+ }
+
+ private boolean isPortAnIpAddress(String address) {
+ try {
+ InetAddress.getByName(address);
+ return true;
+ } catch (UnknownHostException e) {
+ return false;
+ }
+ }
+
+ public void registerStatusListener(@Nullable IModemStatusListener smsModemStatusCallback) {
+ this.modemStatusCallback = smsModemStatusCallback;
+ }
+
+ public void registerMessageListener(@Nullable IInboundOutboundMessageListener messageCallback) {
+ this.messageCallback = messageCallback;
+ }
+
+ public void registerInformationListener(@Nullable IDeviceInformationListener deviceInformationListener) {
+ this.deviceInformation.setDeviceInformationListener(deviceInformationListener);
+ }
+
+ public void processMessage(InboundMessage message) {
+ IInboundOutboundMessageListener messageCallbackFinal = this.messageCallback;
+ if (messageCallbackFinal != null) {
+ messageCallbackFinal.messageReceived(message);
+ }
+ }
+
+ public void processMessageSent(OutboundMessage message) {
+ IInboundOutboundMessageListener messageCallbackFinal = this.messageCallback;
+ if (messageCallbackFinal != null) {
+ messageCallbackFinal.messageSent(message);
+ }
+ }
+
+ public void processDeliveryReport(DeliveryReportMessage message) {
+ IInboundOutboundMessageListener messageCallbackFinal = this.messageCallback;
+ if (messageCallbackFinal != null) {
+ messageCallbackFinal.messageDelivered(message);
+ }
+ }
+
+ public Status getStatus() {
+ return this.status;
+ }
+
+ public final String getGatewayId() {
+ return this.gatewayId;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public void setCapabilities(Capabilities capabilities) {
+ this.capabilities = capabilities;
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/ModemResponse.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/ModemResponse.java
new file mode 100644
index 0000000000000..5f3d1017591b2
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/ModemResponse.java
@@ -0,0 +1,26 @@
+package org.smslib;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class ModemResponse {
+ String responseData;
+
+ boolean responseOk;
+
+ public ModemResponse(String responseData, boolean responseOk) {
+ this.responseData = responseData;
+ this.responseOk = responseOk;
+ }
+
+ public String getResponseData() {
+ return this.responseData;
+ }
+
+ public boolean isResponseOk() {
+ return this.responseOk;
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/UnrecoverableSmslibException.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/UnrecoverableSmslibException.java
new file mode 100644
index 0000000000000..5ae22b8a62598
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/UnrecoverableSmslibException.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.smslib;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * Exception class for internal SMSLib unrecoverable error
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@NonNullByDefault
+public class UnrecoverableSmslibException extends RuntimeException {
+
+ private static final long serialVersionUID = 7649578885702261759L;
+
+ public UnrecoverableSmslibException(String message) {
+ super(message);
+ }
+
+ public UnrecoverableSmslibException(String message, Exception cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/callback/IDeviceInformationListener.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/callback/IDeviceInformationListener.java
new file mode 100644
index 0000000000000..88ffd4ec0be2d
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/callback/IDeviceInformationListener.java
@@ -0,0 +1,34 @@
+package org.smslib.callback;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link IDeviceInformationListener} will receive informations
+ * and statistics
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public interface IDeviceInformationListener {
+
+ void setManufacturer(String manufacturer);
+
+ void setModel(String string);
+
+ void setSwVersion(String swVersion);
+
+ void setSerialNo(String serialNo);
+
+ void setImsi(String imsi);
+
+ void setRssi(String rssi);
+
+ void setMode(String mode);
+
+ public void setTotalSent(String totalSent);
+
+ public void setTotalFailed(String totalFailed);
+
+ public void setTotalReceived(String totalReceived);
+
+ public void setTotalFailures(String totalFailure);
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/callback/IInboundOutboundMessageListener.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/callback/IInboundOutboundMessageListener.java
new file mode 100644
index 0000000000000..4004e80d7a783
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/callback/IInboundOutboundMessageListener.java
@@ -0,0 +1,39 @@
+package org.smslib.callback;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.smslib.message.DeliveryReportMessage;
+import org.smslib.message.InboundMessage;
+import org.smslib.message.OutboundMessage;
+
+/**
+ *
+ * Interface to implement to get messages and reports
+ *
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public interface IInboundOutboundMessageListener {
+
+ /**
+ * Implement this method to get incoming messages
+ *
+ * @param message The inbound message received
+ */
+ public void messageReceived(InboundMessage message);
+
+ /**
+ * Implement this method to get warned when
+ * a message is sent on the network
+ *
+ * @param message the message sent
+ */
+ public void messageSent(OutboundMessage message);
+
+ /**
+ * Implement this method to get warned when
+ * a message previously sent is received by the recipient
+ *
+ * @param message the delivery report message
+ */
+ public void messageDelivered(DeliveryReportMessage message);
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/callback/IModemStatusListener.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/callback/IModemStatusListener.java
new file mode 100644
index 0000000000000..68ef30e47c0b0
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/callback/IModemStatusListener.java
@@ -0,0 +1,15 @@
+package org.smslib.callback;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.smslib.Modem.Status;
+
+/**
+ * Implement this interface to get status change
+ *
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public interface IModemStatusListener {
+
+ boolean processStatusCallback(Status oldStatus, Status newStatus);
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/AbstractModemDriver.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/AbstractModemDriver.java
new file mode 100644
index 0000000000000..05ed3d9c259a2
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/AbstractModemDriver.java
@@ -0,0 +1,540 @@
+package org.smslib.driver;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.StringReader;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.smslib.Capabilities;
+import org.smslib.CommunicationException;
+import org.smslib.Capabilities.Caps;
+import org.smslib.DeviceInformation.Modes;
+import org.smslib.Modem;
+import org.smslib.ModemResponse;
+import org.smslib.UnrecoverableSmslibException;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public abstract class AbstractModemDriver {
+ static Logger logger = LoggerFactory.getLogger(AbstractModemDriver.class);
+
+ private Lock lock = new ReentrantLock();
+
+ Properties modemProperties;
+
+ @NonNullByDefault({})
+ InputStream in;
+
+ @NonNullByDefault({})
+ OutputStream out;
+
+ StringBuffer buffer = new StringBuffer(4096);
+
+ PollReader pollReader = new PollReader(this, "undefined");
+
+ Modem modem;
+
+ boolean responseOk;
+
+ String memoryLocations = "";
+
+ int atATHCounter = 0;
+
+ public abstract void openPort() throws CommunicationException;
+
+ public abstract void closePort();
+
+ public abstract String getPortInfo();
+
+ public AbstractModemDriver(Modem modem) {
+ modemProperties = new Properties();
+ try {
+ ClassLoader classLoader = this.getClass().getClassLoader();
+ if (classLoader != null) {
+ try (InputStream inputStream = classLoader.getResourceAsStream("modem.properties")) {
+ modemProperties.load(inputStream);
+ }
+ }
+ } catch (IOException e) {
+ throw new UnrecoverableSmslibException("Cannot instantiate modem driver", e);
+ }
+ this.modem = modem;
+ }
+
+ public ModemResponse write(String data) throws CommunicationException {
+ return write(data, false);
+ }
+
+ public ModemResponse write(String data, boolean skipResponse) throws CommunicationException {
+ this.lock.lock();
+ try {
+ logger.debug("{} <== {}", getPortInfo(), data);
+ write(data.getBytes());
+ countSheeps(Integer.valueOf(getModemSettings("command_wait_unit")));
+ return (new ModemResponse((skipResponse ? "" : getResponse()), (skipResponse ? true : this.responseOk)));
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ protected boolean hasData() throws IOException {
+ return ((this.in != null) && (this.in.available() > 0));
+ }
+
+ protected int read() throws IOException {
+ return this.in.read();
+ }
+
+ protected void write(byte[] s) throws CommunicationException {
+ int charDelay = Integer.valueOf(getModemSettings("char_wait_unit"));
+ try {
+ if (charDelay == 0) {
+ this.out.write(s);
+ } else {
+ for (int i = 0; i < s.length; i++) {
+ byte b = s[i];
+ this.out.write(b);
+ countSheeps(charDelay);
+ }
+ }
+ } catch (IOException e) {
+ throw new CommunicationException("Cannot write to device", e);
+ }
+ }
+
+ protected void write(byte s) throws CommunicationException {
+ try {
+ this.out.write(s);
+ } catch (IOException e) {
+ throw new CommunicationException("Cannot write data", e);
+ }
+ }
+
+ private String getResponse() throws CommunicationException {
+ StringBuffer raw = new StringBuffer(256);
+ StringBuffer b = new StringBuffer(256);
+ try {
+ while (true) {
+ String line = getLineFromBuffer();
+ logger.debug("{} >>> {}", getPortInfo(), line);
+ this.buffer.delete(0, line.length() + 2);
+ if (line.isBlank()) {
+ continue;
+ }
+ if (line.charAt(0) == '^') {
+ continue;
+ }
+ if (line.charAt(0) == '*') {
+ continue;
+ }
+ if (line.startsWith("RING")) {
+ continue;
+ }
+ if (line.startsWith("+STIN:")) {
+ continue;
+ }
+ if (Integer.valueOf(getModemSettings("cpin_without_ok")) == 1) {
+ if (line.startsWith("+CPIN:")) {
+ raw.append(line);
+ raw.append("$");
+ b.append(line);
+ this.responseOk = true;
+ break;
+ }
+ }
+ if (line.startsWith("+CLIP:")) {
+ write("+++", true);
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
+ write("ATH\r", true);
+ logger.debug("+++ INCREASE ATH");
+ this.atATHCounter++;
+ // no need for a call handler. discard
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
+ continue;
+ }
+ if (line.indexOf("OK") == 0) {
+ if (this.atATHCounter > 0) {
+ logger.debug("--- DECREASE ATH");
+ this.atATHCounter--;
+ continue;
+ }
+ this.responseOk = true;
+ break;
+ }
+ if ((line.indexOf("ERROR") == 0) || (line.indexOf("+CMS ERROR") == 0)
+ || (line.indexOf("+CME ERROR") == 0)) {
+ logger.warn("{} ERR==> {}", getPortInfo(), line);
+ this.responseOk = false;
+ break;
+ }
+ if (b.length() > 0) {
+ b.append('\n');
+ }
+ raw.append(line);
+ raw.append("$");
+ b.append(line);
+ }
+ } catch (IOException | TimeoutException e) {
+ throw new CommunicationException("Cannot get response", e);
+ }
+ logger.debug("{} ==> {}", getPortInfo(), raw.toString());
+ return b.toString();
+ }
+
+ private String getLineFromBuffer() throws TimeoutException, IOException {
+ long startTimeout = System.currentTimeMillis();
+ long endTimeout = startTimeout;
+ while (this.buffer.indexOf("\r") == -1) {
+ endTimeout += Integer.valueOf(getModemSettings("wait_unit"));
+ if ((endTimeout - startTimeout) > Integer.valueOf(getModemSettings("timeout"))) {
+ throw new TimeoutException("Timeout elapsed for " + getPortInfo());
+ }
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
+ }
+ BufferedReader r = new BufferedReader(new StringReader(this.buffer.toString()));
+ String line = r.readLine();
+ r.close();
+ return line;
+ }
+
+ public void clearResponses() {
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit")) * 1);
+ while (this.buffer.length() > 0) {
+ this.buffer.delete(0, this.buffer.length());
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit")) * 1);
+ }
+ }
+
+ public String getMemoryLocations() {
+ return this.memoryLocations;
+ }
+
+ public void initializeModem() throws CommunicationException {
+ int counter = 0;
+ this.lock.lock();
+ try {
+ atAT();
+ atAT();
+ atAT();
+ atAT();
+ atEchoOff();
+ clearResponses();
+ this.modem.getDeviceInformation().setManufacturer(atGetManufacturer().getResponseData());
+ this.modem.getDeviceInformation().setModel(atGetModel().getResponseData());
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
+ atFromModemSettings("init1");
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
+ * Integer.valueOf(getModemSettings("delay_after_init1")));
+ atFromModemSettings("init2");
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
+ * Integer.valueOf(getModemSettings("delay_after_init2")));
+ clearResponses();
+ atEchoOff();
+ clearResponses();
+ atFromModemSettings("pre_pin");
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
+ * Integer.valueOf(getModemSettings("delay_after_pre_pin")));
+ while (true) {
+ counter++;
+ if (counter == 5) {
+ throw new CommunicationException("Modem does not correspond correctly, giving up...");
+ }
+ ModemResponse simStatus = atGetSimStatus();
+ if (simStatus.getResponseData().indexOf("SIM PIN") >= 0) {
+ if (this.modem.getSimPin().isBlank()) {
+ throw new CommunicationException("SIM PIN requested but not defined!");
+ }
+ atEnterPin(this.modem.getSimPin());
+ } else if (simStatus.getResponseData().indexOf("READY") >= 0) {
+ break;
+ } else if (simStatus.getResponseData().indexOf("OK") >= 0) {
+ break;
+ } else if (simStatus.getResponseData().indexOf("ERROR") >= 0) {
+ logger.error("SIM PIN error!");
+ }
+ logger.debug("SIM PIN Not ok, waiting for a while...");
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
+ * Integer.valueOf(getModemSettings("delay_on_sim_error")));
+ }
+ atFromModemSettings("post_pin");
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
+ * Integer.valueOf(getModemSettings("delay_after_post_pin")));
+ atEnableClip();
+ if (!atNetworkRegistration().isResponseOk()) {
+ throw new CommunicationException("Network registration failed!");
+ }
+ atVerboseOff();
+ if (atSetPDUMode().isResponseOk()) {
+ this.modem.getDeviceInformation().setMode(Modes.PDU);
+ } else {
+ logger.debug("Modem does not support PDU, trying to switch to TEXT...");
+ if (atSetTEXTMode().isResponseOk()) {
+ Capabilities caps = new Capabilities();
+ caps.set(Caps.CanSendMessage);
+ this.modem.setCapabilities(caps);
+ this.modem.getDeviceInformation().setMode(Modes.TEXT);
+ } else {
+ throw new CommunicationException("Neither PDU nor TEXT mode are supported by this modem!");
+ }
+ }
+ atCnmiOff();
+ retrieveMemoryLocations();
+ refreshDeviceInformation();
+
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ public void refreshDeviceInformation() throws CommunicationException {
+ this.modem.getDeviceInformation().setManufacturer(atGetManufacturer().getResponseData());
+ this.modem.getDeviceInformation().setModel(atGetModel().getResponseData());
+ this.modem.getDeviceInformation().setSerialNo(atGetSerialNo().getResponseData());
+ this.modem.getDeviceInformation().setImsi(atGetImsi().getResponseData());
+ this.modem.getDeviceInformation().setSwVersion(atGetSWVersion().getResponseData());
+ this.refreshRssi();
+ }
+
+ public void refreshRssi() throws CommunicationException {
+ String s = atGetSignalStrengh().getResponseData();
+ if (this.responseOk) {
+ String s1 = s.split("\\R")[0]; // ensure to get first line only
+ s1 = s1.substring(s.indexOf(':') + 1).trim();
+ StringTokenizer tokens = new StringTokenizer(s1, ",");
+ int rssi = Integer.valueOf(tokens.nextToken().trim());
+ this.modem.getDeviceInformation().setRssi(rssi == 99 ? 99 : (-113 + 2 * rssi));
+ }
+ }
+
+ void retrieveMemoryLocations() throws CommunicationException {
+ if (this.memoryLocations.isBlank()) {
+ this.memoryLocations = getModemSettings("memory_locations");
+ if (this.memoryLocations.isBlank()) {
+ this.memoryLocations = "";
+ }
+ if (this.memoryLocations.isBlank()) {
+ try {
+ String response = atGetMemoryLocations().getResponseData();
+ if (response.indexOf("+CPMS:") >= 0) {
+ int i, j;
+ i = response.indexOf('(');
+ while (response.charAt(i) == '(') {
+ i++;
+ }
+ j = i;
+ while (response.charAt(j) != ')') {
+ j++;
+ }
+ response = response.substring(i, j);
+ StringTokenizer tokens = new StringTokenizer(response, ",");
+ while (tokens.hasMoreTokens()) {
+ String loc = tokens.nextToken().replaceAll("\"", "");
+ if (!"MT".equalsIgnoreCase(loc) && this.memoryLocations.indexOf(loc) < 0) {
+ this.memoryLocations += loc;
+ }
+ }
+ } else {
+ this.memoryLocations = "SM";
+ logger.debug("CPMS detection failed, proceeding with default memory 'SM'.");
+ }
+ } catch (CommunicationException e) {
+ this.memoryLocations = "SM";
+ logger.debug("CPMS detection failed, proceeding with default memory 'SM'.", e);
+ }
+ }
+ } else {
+ logger.debug("Using given memory locations: {}", this.memoryLocations);
+ }
+ }
+
+ public String getSignature(boolean complete) {
+ String manufacturer = this.modem.getDeviceInformation().getManufacturer().toLowerCase().replaceAll(" ", "")
+ .replaceAll(" ", "").replaceAll(" ", "");
+ String model = this.modem.getDeviceInformation().getModel().toLowerCase().replaceAll(" ", "")
+ .replaceAll(" ", "").replaceAll(" ", "");
+ return (complete ? manufacturer + "_" + model : manufacturer);
+ }
+
+ protected ModemResponse atAT() throws CommunicationException {
+ return write("AT\r", true);
+ }
+
+ protected ModemResponse atATWithResponse() throws CommunicationException {
+ return write("AT\r");
+ }
+
+ protected ModemResponse atEchoOff() throws CommunicationException {
+ return write("ATE0\r", true);
+ }
+
+ protected ModemResponse atGetSimStatus() throws CommunicationException {
+ return write("AT+CPIN?\r");
+ }
+
+ protected ModemResponse atEnterPin(String pin) throws CommunicationException {
+ return write(String.format("AT+CPIN=\"%s\"\r", pin));
+ }
+
+ protected ModemResponse atNetworkRegistration() throws CommunicationException {
+ write("AT+CREG=1\r");
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
+ * Integer.valueOf(getModemSettings("delay_network_registration")));
+ return write("AT+CREG?\r");
+ }
+
+ protected ModemResponse atEnableClip() throws CommunicationException {
+ return write("AT+CLIP=1\r");
+ }
+
+ protected ModemResponse atVerboseOff() throws CommunicationException {
+ return write("AT+CMEE=0\r");
+ }
+
+ protected ModemResponse atSetPDUMode() throws CommunicationException {
+ return write("AT+CMGF=0\r");
+ }
+
+ protected ModemResponse atSetTEXTMode() throws CommunicationException {
+ return write("AT+CMGF=1\r");
+ }
+
+ protected ModemResponse atCnmiOff() throws CommunicationException {
+ return write("AT+CNMI=2,0,0,0,0\r");
+ }
+
+ protected ModemResponse atGetManufacturer() throws CommunicationException {
+ return write("AT+CGMI\r");
+ }
+
+ protected ModemResponse atGetModel() throws CommunicationException {
+ return write("AT+CGMM\r");
+ }
+
+ protected ModemResponse atGetImsi() throws CommunicationException {
+ return write("AT+CIMI\r");
+ }
+
+ protected ModemResponse atGetSerialNo() throws CommunicationException {
+ return write("AT+CGSN\r");
+ }
+
+ protected ModemResponse atGetSWVersion() throws CommunicationException {
+ return write("AT+CGMR\r");
+ }
+
+ protected ModemResponse atGetSignalStrengh() throws CommunicationException {
+ return write("AT+CSQ\r");
+ }
+
+ public int atSendPDUMessage(int size, String pdu) throws CommunicationException {
+ write(String.format("AT+CMGS=%d\r", size), true);
+ while (this.buffer.length() == 0) {
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
+ }
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
+ * Integer.valueOf(getModemSettings("delay_before_send_pdu")));
+ clearResponses();
+ write(pdu, true);
+ write((byte) 26);
+ String response = getResponse();
+ if (this.responseOk && response.contains(":")) {
+ return Integer.parseInt(response.substring(response.indexOf(":") + 1).trim());
+ }
+ return -1;
+ }
+
+ public int atSendTEXTMessage(String recipient, String text) throws CommunicationException {
+ write(String.format("AT+CSCS=\"%s\"\r", "UTF-8"), true);
+ if (!this.responseOk) {
+ throw new CommunicationException("Unsupported encoding: UTF-8");
+ }
+ write(String.format("AT+CMGS=\"%s\"\r", recipient), true);
+ while (this.buffer.length() == 0) {
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit")));
+ }
+ countSheeps(Integer.valueOf(getModemSettings("wait_unit"))
+ * Integer.valueOf(getModemSettings("delay_before_send_pdu")));
+ clearResponses();
+ write(text, true);
+ write((byte) 26);
+ String response = getResponse();
+ if (this.responseOk) {
+ return Integer.parseInt(response.substring(response.indexOf(":") + 1).trim());
+ }
+ return -1;
+ }
+
+ public ModemResponse atGetMemoryLocations() throws CommunicationException {
+ return write("AT+CPMS=?\r");
+ }
+
+ public ModemResponse atSwitchMemoryLocation(String memoryLocation) throws CommunicationException {
+ return write(String.format("AT+CPMS=\"%s\"\r", memoryLocation));
+ }
+
+ public ModemResponse atGetMessages(String memoryLocation) throws CommunicationException {
+ if (atSwitchMemoryLocation(memoryLocation).isResponseOk()) {
+ return (this.modem.getDeviceInformation().getMode() == Modes.PDU ? write("AT+CMGL=4\r")
+ : write("AT+CMGL=\"ALL\"\r"));
+ }
+ return new ModemResponse("", false);
+ }
+
+ public ModemResponse atDeleteMessage(String memoryLocation, int memoryIndex) throws CommunicationException {
+ if (atSwitchMemoryLocation(memoryLocation).isResponseOk()) {
+ return write(String.format("AT+CMGD=%d\r", memoryIndex));
+ }
+ return new ModemResponse("", false);
+ }
+
+ public ModemResponse atFromModemSettings(String key) throws CommunicationException {
+ String atCommand = getModemSettings(key);
+ if (!atCommand.isBlank()) {
+ return write(atCommand);
+ }
+ return new ModemResponse("", true);
+ }
+
+ public String getModemSettings(String key) {
+ String fullSignature = getSignature(true);
+ String shortSignature = getSignature(false);
+ String value = "";
+ if (!fullSignature.isBlank()) {
+ value = modemProperties.getProperty(fullSignature + "." + key);
+ }
+ if ((value == null || value.isBlank()) && !shortSignature.isBlank()) {
+ value = modemProperties.getProperty(shortSignature + "." + key);
+ }
+ if (value == null || value.isBlank()) {
+ value = modemProperties.getProperty("default" + "." + key);
+ }
+ return ((value == null || value.isBlank()) ? "" : value);
+ }
+
+ public void lock() {
+ this.lock.lock();
+ }
+
+ public void unlock() {
+ this.lock.unlock();
+ }
+
+ protected static void countSheeps(int n) {
+ try {
+ Thread.sleep(n);
+ } catch (InterruptedException e) {
+ // Nothing here...
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/IPModemDriver.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/IPModemDriver.java
new file mode 100644
index 0000000000000..f91f4b82d6839
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/IPModemDriver.java
@@ -0,0 +1,85 @@
+package org.smslib.driver;
+
+import java.io.IOException;
+import java.net.Socket;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.smslib.CommunicationException;
+import org.smslib.Modem;
+
+/**
+ * Extracted from SMSLib
+ * Manage communication with ser2net (or equivalent)
+ *
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class IPModemDriver extends AbstractModemDriver {
+ static Logger logger = LoggerFactory.getLogger(IPModemDriver.class);
+
+ String address;
+
+ int port;
+
+ @Nullable
+ Socket socket;
+
+ public IPModemDriver(Modem modem, String address, int port) {
+ super(modem);
+ this.address = address;
+ this.port = port;
+ }
+
+ @Override
+ public void openPort() throws CommunicationException {
+ logger.debug("Opening IP port: {}", getPortInfo());
+ try {
+ Socket openSocket = new Socket(this.address, this.port);
+ openSocket.setReceiveBufferSize(Integer.valueOf(getModemSettings("port_buffer")));
+ openSocket.setSendBufferSize(Integer.valueOf(getModemSettings("port_buffer")));
+ openSocket.setSoTimeout(30000);
+ openSocket.setTcpNoDelay(true);
+ this.in = openSocket.getInputStream();
+ this.out = openSocket.getOutputStream();
+ this.socket = openSocket;
+ } catch (IOException e) {
+ throw new CommunicationException("Cannot open port", e);
+ }
+ countSheeps(Integer.valueOf(getModemSettings("after_ip_connect_wait_unit")));
+ this.pollReader = new PollReader(this, getPortInfo());
+ this.pollReader.setDaemon(true);
+ this.pollReader.start();
+ }
+
+ @Override
+ public void closePort() {
+ logger.debug("Closing IP port: {}", getPortInfo());
+ try {
+ this.pollReader.cancel();
+ this.pollReader.join();
+ if (in != null) {
+ this.in.close();
+ this.in = null;
+ }
+ if (out != null) {
+ this.out.close();
+ this.out = null;
+ }
+ Socket finalSocket = socket;
+ if (finalSocket != null) {
+ finalSocket.close();
+ }
+ } catch (InterruptedException | IOException e) {
+ logger.debug("Cannot close port");
+ }
+ countSheeps(Integer.valueOf(getModemSettings("after_ip_connect_wait_unit")));
+ }
+
+ @Override
+ public String getPortInfo() {
+ return this.address + ":" + this.port;
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/JSerialModemDriver.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/JSerialModemDriver.java
new file mode 100644
index 0000000000000..e64a8e713d426
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/JSerialModemDriver.java
@@ -0,0 +1,101 @@
+package org.smslib.driver;
+
+import java.io.IOException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.io.transport.serial.PortInUseException;
+import org.openhab.core.io.transport.serial.SerialPort;
+import org.openhab.core.io.transport.serial.SerialPortIdentifier;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.smslib.CommunicationException;
+import org.smslib.Modem;
+
+/**
+ * Manage communications with a serial modem
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class JSerialModemDriver extends AbstractModemDriver {
+
+ private static final int ONE_STOP_BIT = 1;
+ static final public int NO_PARITY = 0;
+ static final public int FLOW_CONTROL_RTS_ENABLED = 0x00000001;
+ static final public int FLOW_CONTROL_CTS_ENABLED = 0x00000010;
+
+ static Logger logger = LoggerFactory.getLogger(JSerialModemDriver.class);
+
+ String portName;
+
+ int baudRate;
+
+ private final SerialPortManager serialPortManager;
+
+ @Nullable
+ SerialPort serialPort;
+
+ public JSerialModemDriver(SerialPortManager serialPortManager, Modem modem, String port, int baudRate) {
+ super(modem);
+ this.portName = port;
+ this.baudRate = baudRate;
+ this.serialPortManager = serialPortManager;
+ }
+
+ @Override
+ public void openPort() throws CommunicationException {
+ SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(portName);
+ if (portIdentifier == null) {
+ throw new CommunicationException("SMSModem cannot use serial port " + portName);
+ }
+ try {
+ SerialPort openedSerialPort = portIdentifier.open("org.openhab.binding.smsmodem", 2000);
+ openedSerialPort.setSerialPortParams(baudRate, 8, ONE_STOP_BIT, NO_PARITY);
+ openedSerialPort.setFlowControlMode(FLOW_CONTROL_RTS_ENABLED | FLOW_CONTROL_CTS_ENABLED);
+ this.in = openedSerialPort.getInputStream();
+ this.out = openedSerialPort.getOutputStream();
+ serialPort = openedSerialPort;
+ this.pollReader = new PollReader(this, getPortInfo());
+ this.pollReader.setDaemon(true);
+ this.pollReader.start();
+
+ } catch (PortInUseException | UnsupportedCommOperationException | IOException e) {
+ throw new CommunicationException("Cannot open port", e);
+ }
+ }
+
+ @Override
+ public void closePort() {
+ try {
+ logger.debug("Closing comm port: {}", getPortInfo());
+ this.pollReader.cancel();
+ try {
+ this.pollReader.join();
+ } catch (InterruptedException ex) {
+ logger.debug("PollReader closing exception", ex);
+ }
+ if (in != null) {
+ this.in.close();
+ this.in = null;
+ }
+ if (out != null) {
+ this.out.close();
+ this.out = null;
+ }
+ final SerialPort finalSerialPort = serialPort;
+ if (finalSerialPort != null) {
+ finalSerialPort.close();
+ serialPort = null;
+ }
+ } catch (IOException e) {
+ logger.debug("Closing port exception", e);
+ }
+ }
+
+ @Override
+ public String getPortInfo() {
+ return this.portName + ":" + this.baudRate;
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/PollReader.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/PollReader.java
new file mode 100644
index 0000000000000..4e1aba4d72c70
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/driver/PollReader.java
@@ -0,0 +1,75 @@
+package org.smslib.driver;
+
+import java.io.IOException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.smslib.CommunicationException;
+
+/**
+ * Manage communications with a serial modem
+ * Extracted from SMSLib
+ */
+public class PollReader extends Thread {
+
+ static Logger logger = LoggerFactory.getLogger(AbstractModemDriver.class);
+
+ private boolean shouldCancel = false;
+
+ private boolean foundClip = false;
+
+ public PollReader(AbstractModemDriver modemDriver, String threadId) {
+ super();
+ this.modemDriver = modemDriver;
+ this.threadId = threadId;
+ }
+
+ private AbstractModemDriver modemDriver;
+
+ private String threadId;
+
+ public void cancel() {
+ this.shouldCancel = true;
+ this.interrupt();
+ }
+
+ @Override
+ public void run() {
+ logger.debug("Started!");
+ currentThread().setName("OH-binding-smsmodem-" + threadId);
+ while (!this.shouldCancel) {
+ try {
+ while (modemDriver.hasData()) {
+ char c = (char) modemDriver.read();
+ modemDriver.buffer.append(c);
+ if (modemDriver.buffer.indexOf("+CLIP") >= 0) {
+ if (!this.foundClip) {
+ this.foundClip = true;
+ new ClipReader().start();
+ }
+ } else {
+ this.foundClip = false;
+ }
+ }
+ } catch (IOException e) {
+ logger.debug("Cannot proceed to poll device", e);
+ modemDriver.modem.error();
+ }
+ AbstractModemDriver.countSheeps(Integer.valueOf(modemDriver.getModemSettings("poll_reader")));
+ }
+ logger.debug("Stopped!");
+ }
+
+ public class ClipReader extends Thread {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(1000);
+ modemDriver.atATWithResponse();
+ } catch (InterruptedException | CommunicationException e) {
+ logger.debug("Cannot proceed to read clip", e);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/AbstractMessage.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/AbstractMessage.java
new file mode 100644
index 0000000000000..d358faaafe06a
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/AbstractMessage.java
@@ -0,0 +1,226 @@
+package org.smslib.message;
+
+import java.io.Serializable;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Date;
+import java.util.UUID;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.smslib.UnrecoverableSmslibException;
+import org.smslib.message.OutboundMessage.SentStatus;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public abstract class AbstractMessage implements Serializable {
+ public enum Encoding {
+ Enc7,
+ Enc8,
+ EncUcs2,
+ EncCustom;
+ }
+
+ public enum DcsClass {
+ None,
+ Flash,
+ Me,
+ Sim,
+ Te
+ }
+
+ public enum Type {
+ Inbound,
+ Outbound,
+ StatusReport
+ }
+
+ private static final long serialVersionUID = 1L;
+
+ Date creationDate = new Date();
+
+ String id = UUID.randomUUID().toString();
+
+ MsIsdn originatorAddress = new MsIsdn();
+
+ @Nullable
+ MsIsdn recipientAddress = new MsIsdn();
+
+ Payload payload = new Payload("");
+
+ Type type = Type.Inbound;
+
+ Encoding encoding = Encoding.Enc7;
+
+ DcsClass dcsClass = DcsClass.Sim;
+
+ String gatewayId = "";
+
+ int sourcePort = -1;
+
+ int destinationPort = -1;
+
+ @Nullable
+ Date sentDate;
+
+ public AbstractMessage() {
+ }
+
+ public AbstractMessage(Type type, MsIsdn originatorAddress, @Nullable MsIsdn recipientAddress,
+ @Nullable Payload payload) {
+ this.type = type;
+ this.originatorAddress = originatorAddress;
+ this.recipientAddress = recipientAddress;
+ if (payload != null) {
+ setPayload(payload);
+ }
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public Date getCreationDate() {
+ return this.creationDate;
+ }
+
+ public MsIsdn getOriginatorAddress() {
+ return this.originatorAddress;
+ }
+
+ public @Nullable MsIsdn getRecipientAddress() {
+ return this.recipientAddress;
+ }
+
+ public Payload getPayload() {
+ return this.payload;
+ }
+
+ public void setPayload(Payload payload) {
+ this.payload = payload;
+ }
+
+ public Type getType() {
+ return this.type;
+ }
+
+ public Encoding getEncoding() {
+ return this.encoding;
+ }
+
+ public void setEncoding(Encoding encoding) {
+ this.encoding = encoding;
+ }
+
+ public DcsClass getDcsClass() {
+ return this.dcsClass;
+ }
+
+ public void setDcsClass(DcsClass dcsClass) {
+ this.dcsClass = dcsClass;
+ }
+
+ public String getGatewayId() {
+ return this.gatewayId;
+ }
+
+ public void setGatewayId(String gatewayId) {
+ this.gatewayId = gatewayId;
+ }
+
+ public int getSourcePort() {
+ return this.sourcePort;
+ }
+
+ public void setSourcePort(int sourcePort) {
+ this.sourcePort = sourcePort;
+ }
+
+ public int getDestinationPort() {
+ return this.destinationPort;
+ }
+
+ public void setDestinationPort(int destinationPort) {
+ this.destinationPort = destinationPort;
+ }
+
+ public @Nullable Date getSentDate() {
+ Date sentDateFinal = this.sentDate;
+ return (sentDateFinal != null ? (Date) sentDateFinal.clone() : null);
+ }
+
+ public void setSentDate(Date sentDate) {
+ this.sentDate = new Date(sentDate.getTime());
+ }
+
+ public abstract String getSignature();
+
+ public abstract String toShortString();
+
+ public String hashSignature(String s) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ md.update(s.getBytes(), 0, s.length());
+ BigInteger i = new BigInteger(1, md.digest());
+ return String.format("%1$032x", i);
+ } catch (NoSuchAlgorithmException e) {
+ throw new UnrecoverableSmslibException("Cannot find hash algorithm", e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer b = new StringBuffer(1024);
+ b.append(String
+ .format("%n== MESSAGE START ======================================================================%n"));
+ b.append(String.format("CLASS: %s%n", this.getClass().toString()));
+ b.append(String.format("Message ID: %s%n", getId()));
+ b.append(String.format("Message Signature: %s%n", getSignature()));
+ b.append(String.format("Via Gateway: %s%n", getGatewayId()));
+ b.append(String.format("Creation Date: %s%n", getCreationDate()));
+ b.append(String.format("Type: %s%n", getType()));
+ b.append(String.format("Encoding: %s%n", getEncoding()));
+ b.append(String.format("DCS Class: %s%n", getDcsClass()));
+ b.append(String.format("Source Port: %s%n", getSourcePort()));
+ b.append(String.format("Destination Port: %s%n", getDestinationPort()));
+ b.append(String.format("Originator Address: %s%n", getOriginatorAddress()));
+ b.append(String.format("Recipient Address: %s%n", getRecipientAddress()));
+ b.append(String.format("Payload Type: %s%n", payload.getType()));
+ b.append(String.format("Text payload: %s%n", payload.getText() == null ? "null" : payload.getText()));
+ if (this instanceof InboundMessage) {
+ b.append(String.format("Sent Date: %s%n", getSentDate()));
+ b.append(String.format("Memory Storage Location: %s%n", ((InboundMessage) this).getMemLocation()));
+ b.append(String.format("Memory Index: %d%n", ((InboundMessage) this).getMemIndex()));
+ b.append(String.format("Memory MP Index: %s%n", ((InboundMessage) this).getMpMemIndex()));
+ }
+ if (this instanceof OutboundMessage) {
+ b.append(String.format("Sent Date: %s%n",
+ (((OutboundMessage) this).getSentStatus() == SentStatus.Sent ? getSentDate() : "N/A")));
+ String ids = "";
+ for (String opId : ((OutboundMessage) this).getOperatorMessageIds()) {
+ ids += (ids.length() == 0 ? opId : "," + opId);
+ }
+ b.append(String.format("Operator Message IDs: %s%n", ids));
+ b.append(String.format("Status: %s%n", ((OutboundMessage) this).getSentStatus().toString()));
+ b.append(String.format("Failure: %s%n", ((OutboundMessage) this).getFailureCause().toString()));
+ b.append(String.format("Request Delivery Reports: %b%n",
+ ((OutboundMessage) this).getRequestDeliveryReport()));
+ }
+ if (this instanceof DeliveryReportMessage) {
+ b.append(String.format("Original Operator Message Id: %s%n",
+ ((DeliveryReportMessage) this).getOriginalOperatorMessageId()));
+ b.append(String.format("Delivery Date: %s%n", ((DeliveryReportMessage) this).getOriginalReceivedDate()));
+ b.append(String.format("Delivery Status: %s%n", ((DeliveryReportMessage) this).getDeliveryStatus()));
+ }
+ b.append(String
+ .format("== MESSAGE END ========================================================================%n"));
+ return b.toString();
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/DeliveryReportMessage.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/DeliveryReportMessage.java
new file mode 100644
index 0000000000000..29af720c7f7ab
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/DeliveryReportMessage.java
@@ -0,0 +1,122 @@
+package org.smslib.message;
+
+import java.util.Date;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.smslib.pduUtils.gsm3040.SmsStatusReportPdu;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class DeliveryReportMessage extends InboundMessage {
+ private static final long serialVersionUID = 1L;
+
+ public enum DeliveryStatus {
+ Unknown("U"),
+ Pending("P"),
+ Failed("F"),
+ Delivered("D"),
+ Expired("X"),
+ Error("E");
+
+ private final String shortString;
+
+ private DeliveryStatus(String shortString) {
+ this.shortString = shortString;
+ }
+
+ public String toShortString() {
+ return this.shortString;
+ }
+ }
+
+ DeliveryStatus deliveryStatus = DeliveryStatus.Unknown;
+
+ @Nullable
+ String originalOperatorMessageId;
+
+ @Nullable
+ Date originalReceivedDate;
+
+ public DeliveryReportMessage() {
+ super(Type.StatusReport, "", 0);
+ }
+
+ public DeliveryReportMessage(SmsStatusReportPdu pdu, String memLocation, int memIndex) {
+ super(Type.StatusReport, memLocation, memIndex);
+ setOriginalOperatorMessageId(String.valueOf(pdu.getMessageReference()));
+ String address = pdu.getAddress();
+ if (address == null) {
+ throw new IllegalArgumentException("Recipient address cannot be null");
+ }
+ this.recipientAddress = new MsIsdn(address);
+ Date timestamp = pdu.getTimestamp();
+ if (timestamp == null) {
+ throw new IllegalArgumentException("Cannot get timestamp for delivery report message");
+ }
+ setSentDate(timestamp);
+ Date dischargeTime = pdu.getDischargeTime();
+ if (dischargeTime == null) {
+ throw new IllegalArgumentException("Cannot get discharge time for delivery report message");
+ }
+ setOriginalReceivedDate(dischargeTime);
+ int i = pdu.getStatus();
+ setPayload(new Payload(""));
+ if ((i & 0x60) == 0) {
+ this.deliveryStatus = DeliveryStatus.Delivered;
+ } else if ((i & 0x20) == 0x20) {
+ this.deliveryStatus = DeliveryStatus.Pending;
+ } else if ((i & 0x40) == 0x40) {
+ this.deliveryStatus = DeliveryStatus.Expired;
+ } else if ((i & 0x60) == 0x60) {
+ this.deliveryStatus = DeliveryStatus.Expired;
+ } else {
+ this.deliveryStatus = DeliveryStatus.Error;
+ }
+ }
+
+ public DeliveryReportMessage(String messageId, String recipientAddress, String memLocation, int memIndex,
+ Date originalSentDate, Date receivedDate) {
+ super(Type.StatusReport, memLocation, memIndex);
+ setOriginalOperatorMessageId(messageId);
+ this.recipientAddress = new MsIsdn(recipientAddress);
+ setSentDate(originalSentDate);
+ setOriginalReceivedDate(receivedDate);
+ this.deliveryStatus = DeliveryStatus.Unknown;
+ }
+
+ public DeliveryStatus getDeliveryStatus() {
+ return this.deliveryStatus;
+ }
+
+ public @Nullable String getOriginalOperatorMessageId() {
+ return this.originalOperatorMessageId;
+ }
+
+ public void setOriginalOperatorMessageId(String originalOperatorMessageId) {
+ this.originalOperatorMessageId = originalOperatorMessageId;
+ }
+
+ public @Nullable Date getOriginalReceivedDate() {
+ Date finalOriginalReceivedDate = originalReceivedDate;
+ return finalOriginalReceivedDate == null ? null : new Date(finalOriginalReceivedDate.getTime());
+ }
+
+ public void setOriginalReceivedDate(Date originalReceivedDate) {
+ this.originalReceivedDate = new Date(originalReceivedDate.getTime());
+ }
+
+ @Override
+ public String getSignature() {
+ return hashSignature(String.format("%s-%s-%s-%s", getOriginatorAddress(), getOriginalOperatorMessageId(),
+ getOriginalReceivedDate(), getDeliveryStatus()));
+ }
+
+ @Override
+ public String toShortString() {
+ return String.format("[%s @ %s = %s @ %s]", getId(), getRecipientAddress(), getDeliveryStatus(),
+ getOriginalReceivedDate());
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/InboundBinaryMessage.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/InboundBinaryMessage.java
new file mode 100644
index 0000000000000..b9dbdadcb5702
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/InboundBinaryMessage.java
@@ -0,0 +1,17 @@
+package org.smslib.message;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.smslib.pduUtils.gsm3040.SmsDeliveryPdu;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class InboundBinaryMessage extends InboundMessage {
+ private static final long serialVersionUID = 1L;
+
+ public InboundBinaryMessage(SmsDeliveryPdu pdu, String memLocation, int memIndex) {
+ super(pdu, memLocation, memIndex);
+ setPayload(new Payload(pdu.getUserDataAsBytes()));
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/InboundMessage.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/InboundMessage.java
new file mode 100644
index 0000000000000..8ab6af52560a8
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/InboundMessage.java
@@ -0,0 +1,164 @@
+package org.smslib.message;
+
+import java.util.Date;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.smslib.pduUtils.gsm3040.PduUtils;
+import org.smslib.pduUtils.gsm3040.SmsDeliveryPdu;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class InboundMessage extends AbstractMessage {
+ static Logger logger = LoggerFactory.getLogger(InboundMessage.class);
+
+ private static final long serialVersionUID = 1L;
+
+ int memIndex;
+
+ String memLocation;
+
+ int mpRefNo;
+
+ int mpMaxNo;
+
+ int mpSeqNo;
+
+ String mpMemIndex = "";
+
+ @Nullable
+ MsIsdn smscNumber;
+
+ boolean endsWithMultiChar;
+
+ public InboundMessage(SmsDeliveryPdu pdu, String memLocation, int memIndex) {
+ super(Type.Inbound, new MsIsdn(pdu.getAddress()), null, null);
+ this.memLocation = memLocation;
+ this.memIndex = memIndex;
+ this.mpRefNo = 0;
+ this.mpMaxNo = 0;
+ this.mpSeqNo = 0;
+ setMpMemIndex(-1);
+ int dcsEncoding = PduUtils.extractDcsEncoding(pdu.getDataCodingScheme());
+ switch (dcsEncoding) {
+ case PduUtils.DCS_ENCODING_7BIT:
+ setEncoding(Encoding.Enc7);
+ break;
+ case PduUtils.DCS_ENCODING_8BIT:
+ setEncoding(Encoding.Enc8);
+ break;
+ case PduUtils.DCS_ENCODING_UCS2:
+ setEncoding(Encoding.EncUcs2);
+ break;
+ default:
+ logger.error("Unknown DCS Encoding: {}", dcsEncoding);
+ }
+ Date timestamp = pdu.getTimestamp();
+ if (timestamp != null) {
+ setSentDate(timestamp);
+ }
+ this.smscNumber = new MsIsdn(pdu.getSmscAddress());
+ setPayload(new Payload(pdu.getDecodedText()));
+ if (pdu.isConcatMessage()) {
+ this.mpRefNo = pdu.getMpRefNo();
+ this.mpMaxNo = pdu.getMpMaxNo();
+ this.mpSeqNo = pdu.getMpSeqNo();
+ }
+ if (pdu.isPortedMessage()) {
+ setSourcePort(pdu.getSrcPort());
+ setDestinationPort(pdu.getDestPort());
+ }
+ if (getEncoding() == Encoding.Enc7) {
+ byte[] udData = pdu.getUDData();
+ if (udData == null) {
+ throw new IllegalArgumentException("Cannot encode udData to construct message");
+ }
+ byte[] temp = PduUtils.encodedSeptetsToUnencodedSeptets(udData);
+ if (temp.length == 0) {
+ this.endsWithMultiChar = false;
+ } else if (temp[temp.length - 1] == 0x1b) {
+ this.endsWithMultiChar = true;
+ }
+ }
+ }
+
+ public InboundMessage(String originator, String text, Date sentDate, String memLocation, int memIndex) {
+ super(Type.Inbound, new MsIsdn(originator), null, new Payload(text));
+ this.memLocation = memLocation;
+ this.memIndex = memIndex;
+ this.sentDate = new Date(sentDate.getTime());
+ }
+
+ public InboundMessage(Type type, String memLocation, int memIndex) {
+ super(type, new MsIsdn(), null, null);
+ this.memIndex = memIndex;
+ this.memLocation = memLocation;
+ this.mpRefNo = 0;
+ this.mpMaxNo = 0;
+ this.mpSeqNo = 0;
+ setMpMemIndex(-1);
+ this.smscNumber = new MsIsdn();
+ }
+
+ public int getMemIndex() {
+ return this.memIndex;
+ }
+
+ public void setMemIndex(int memIndex) {
+ this.memIndex = memIndex;
+ }
+
+ public String getMemLocation() {
+ return this.memLocation;
+ }
+
+ public int getMpMaxNo() {
+ return this.mpMaxNo;
+ }
+
+ public String getMpMemIndex() {
+ return this.mpMemIndex;
+ }
+
+ public void setMpMemIndex(int myMpMemIndex) {
+ if (myMpMemIndex == -1) {
+ this.mpMemIndex = "";
+ } else {
+ this.mpMemIndex += (this.mpMemIndex.length() == 0 ? "" : ",") + myMpMemIndex;
+ }
+ }
+
+ public int getMpRefNo() {
+ return this.mpRefNo;
+ }
+
+ public int getMpSeqNo() {
+ return this.mpSeqNo;
+ }
+
+ public void setMpSeqNo(int myMpSeqNo) {
+ this.mpSeqNo = myMpSeqNo;
+ }
+
+ public boolean getEndsWithMultiChar() {
+ return this.endsWithMultiChar;
+ }
+
+ public void setEndsWithMultiChar(boolean b) {
+ this.endsWithMultiChar = b;
+ }
+
+ @Override
+ public String getSignature() {
+ return hashSignature(String.format("%s-%s-%s", getOriginatorAddress(), getSentDate(), payload.getText()));
+ }
+
+ @Override
+ public String toShortString() {
+ return String.format("[%s @ %s]", getId(), getOriginatorAddress());
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/MsIsdn.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/MsIsdn.java
new file mode 100644
index 0000000000000..630f51685bcd4
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/MsIsdn.java
@@ -0,0 +1,89 @@
+package org.smslib.message;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class MsIsdn {
+ public enum Type {
+ National,
+ International,
+ Text,
+ Void
+ }
+
+ String address;
+
+ Type type = Type.International;
+
+ public MsIsdn() {
+ this("", Type.Void);
+ }
+
+ public MsIsdn(@Nullable String number) {
+ if (number == null) {
+ throw new IllegalArgumentException("Number cannot be null");
+ }
+ if (number.length() > 0 && number.charAt(0) == '+') {
+ this.address = number.substring(1);
+ this.type = Type.International;
+ } else {
+ this.address = number;
+ this.type = typeOf(number);
+ }
+ }
+
+ public MsIsdn(String address, Type type) {
+ this.address = address;
+ this.type = type;
+ }
+
+ public MsIsdn(MsIsdn msisdn) {
+ this.type = msisdn.getType();
+ this.address = msisdn.getAddress();
+ }
+
+ public String getAddress() {
+ return this.address;
+ }
+
+ public Type getType() {
+ return this.type;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof MsIsdn)) {
+ return false;
+ }
+ return (this.address.equalsIgnoreCase(((MsIsdn) o).getAddress()));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s / %s]", getType(), getAddress());
+ }
+
+ @Override
+ public int hashCode() {
+ return this.address.hashCode() + (15 * this.type.hashCode());
+ }
+
+ private static Type typeOf(String number) {
+ if (number.trim().length() == 0) {
+ return Type.Void;
+ }
+ for (int i = 0; i < number.length(); i++) {
+ if (!Character.isDigit(number.charAt(i))) {
+ return Type.Text;
+ }
+ }
+ return Type.International;
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/OutboundBinaryMessage.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/OutboundBinaryMessage.java
new file mode 100644
index 0000000000000..e5dd2dfc2c15b
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/OutboundBinaryMessage.java
@@ -0,0 +1,19 @@
+package org.smslib.message;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class OutboundBinaryMessage extends OutboundMessage {
+ private static final long serialVersionUID = 1L;
+
+ public OutboundBinaryMessage() {
+ }
+
+ public OutboundBinaryMessage(MsIsdn originatorAddress, MsIsdn recipientAddress, byte[] data) {
+ super(originatorAddress, recipientAddress, new Payload(data));
+ setEncoding(Encoding.Enc8);
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/OutboundMessage.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/OutboundMessage.java
new file mode 100644
index 0000000000000..a677d035c425c
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/OutboundMessage.java
@@ -0,0 +1,193 @@
+package org.smslib.message;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.smslib.UnrecoverableSmslibException;
+import org.smslib.pduUtils.gsm3040.PduFactory;
+import org.smslib.pduUtils.gsm3040.PduGenerator;
+import org.smslib.pduUtils.gsm3040.PduUtils;
+import org.smslib.pduUtils.gsm3040.SmsSubmitPdu;
+import org.smslib.pduUtils.gsm3040.ie.InformationElementFactory;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class OutboundMessage extends AbstractMessage {
+ private static final long serialVersionUID = 1L;
+
+ public enum SentStatus {
+ Sent("S"),
+ Unsent("U"),
+ Queued("Q"),
+ Failed("F");
+
+ private final String shortString;
+
+ private SentStatus(String shortString) {
+ this.shortString = shortString;
+ }
+
+ public String toShortString() {
+ return this.shortString;
+ }
+ }
+
+ public enum FailureCause {
+ None("00"),
+ BadNumber("01"),
+ BadFormat("02"),
+ GatewayFailure("03"),
+ AuthFailure("04"),
+ NoCredit("05"),
+ OverQuota("06"),
+ NoRoute("07"),
+ Unavailable("08"),
+ HttpError("09"),
+ UnknownFailure("10"),
+ Cancelled("11"),
+ NoService("12"),
+ MissingParms("13");
+
+ private final String shortString;
+
+ private FailureCause(String shortString) {
+ this.shortString = shortString;
+ }
+
+ public String toShortString() {
+ return this.shortString;
+ }
+ }
+
+ SentStatus sentStatus = SentStatus.Unsent;
+
+ FailureCause failureCause = FailureCause.None;
+
+ List operatorMessageIds = new ArrayList<>();
+
+ boolean requestDeliveryReport = false;
+
+ public OutboundMessage() {
+ }
+
+ public OutboundMessage(MsIsdn originatorAddress, MsIsdn recipientAddress, Payload payload) {
+ super(Type.Outbound, originatorAddress, recipientAddress, payload);
+ }
+
+ public OutboundMessage(String recipientAddress, String text) {
+ this(new MsIsdn(""), new MsIsdn(recipientAddress), new Payload(text));
+ }
+
+ public SentStatus getSentStatus() {
+ return this.sentStatus;
+ }
+
+ public void setSentStatus(SentStatus sentStatus) {
+ this.sentStatus = sentStatus;
+ }
+
+ public FailureCause getFailureCause() {
+ return this.failureCause;
+ }
+
+ public void setFailureCause(FailureCause failureCode) {
+ this.failureCause = failureCode;
+ }
+
+ public List getOperatorMessageIds() {
+ return this.operatorMessageIds;
+ }
+
+ public boolean getRequestDeliveryReport() {
+ return this.requestDeliveryReport;
+ }
+
+ public void setRequestDeliveryReport(boolean requestDeliveryReport) {
+ this.requestDeliveryReport = requestDeliveryReport;
+ }
+
+ @Override
+ public String toShortString() {
+ return String.format("[%s @ %s]", getId(), getRecipientAddress());
+ }
+
+ public List getPdus(MsIsdn smscNumber, int mpRefNo) {
+ PduGenerator pduGenerator = new PduGenerator();
+ SmsSubmitPdu pdu = createPduObject(getRequestDeliveryReport());
+ initPduObject(pdu, smscNumber);
+ return pduGenerator.generatePduList(pdu, mpRefNo);
+ }
+
+ protected SmsSubmitPdu createPduObject(boolean extRequestDeliveryReport) {
+ return (extRequestDeliveryReport ? PduFactory.newSmsSubmitPdu(PduUtils.TP_SRR_REPORT | PduUtils.TP_VPF_INTEGER)
+ : PduFactory.newSmsSubmitPdu());
+ }
+
+ protected void initPduObject(SmsSubmitPdu pdu, MsIsdn smscNumber) {
+ if ((getSourcePort() > -1) && (getDestinationPort() > -1)) {
+ pdu.addInformationElement(
+ InformationElementFactory.generatePortInfo(getDestinationPort(), getSourcePort()));
+ }
+ String smscNumberForLengthCheck = smscNumber.getAddress();
+ pdu.setSmscInfoLength(
+ 1 + (smscNumberForLengthCheck.length() / 2) + ((smscNumberForLengthCheck.length() % 2 == 1) ? 1 : 0));
+ pdu.setSmscAddress(smscNumber.getAddress());
+ pdu.setSmscAddressType(PduUtils.getAddressTypeFor(smscNumber));
+ pdu.setMessageReference(0);
+ MsIsdn finalRecipientAddress = recipientAddress;
+ if (finalRecipientAddress == null) {
+ throw new UnrecoverableSmslibException("Recipient adress cannot be null");
+ }
+ pdu.setAddress(finalRecipientAddress);
+ MsIsdn recipientAddressFinal = this.recipientAddress;
+ if (recipientAddressFinal == null) {
+ throw new UnrecoverableSmslibException("Cannot set address type with no recipient");
+ }
+ pdu.setAddressType(PduUtils.getAddressTypeFor(recipientAddressFinal));
+ pdu.setProtocolIdentifier(0);
+ if (!pdu.isBinary()) {
+ int dcs = 0;
+ if (getEncoding() == Encoding.Enc7) {
+ dcs = PduUtils.DCS_ENCODING_7BIT;
+ } else if (getEncoding() == Encoding.Enc8) {
+ dcs = PduUtils.DCS_ENCODING_8BIT;
+ } else if (getEncoding() == Encoding.EncUcs2) {
+ dcs = PduUtils.DCS_ENCODING_UCS2;
+ } else if (getEncoding() == Encoding.EncCustom) {
+ dcs = PduUtils.DCS_ENCODING_7BIT;
+ }
+ if (getDcsClass() == DcsClass.Flash) {
+ dcs = dcs | PduUtils.DCS_MESSAGE_CLASS_FLASH;
+ } else if (getDcsClass() == DcsClass.Me) {
+ dcs = dcs | PduUtils.DCS_MESSAGE_CLASS_ME;
+ } else if (getDcsClass() == DcsClass.Sim) {
+ dcs = dcs | PduUtils.DCS_MESSAGE_CLASS_SIM;
+ } else if (getDcsClass() == DcsClass.Te) {
+ dcs = dcs | PduUtils.DCS_MESSAGE_CLASS_TE;
+ }
+ pdu.setDataCodingScheme(dcs);
+ }
+ pdu.setValidityPeriod(0);
+ if (getEncoding() == Encoding.Enc8) {
+ byte[] bytes = getPayload().getBytes();
+ if (bytes == null) {
+ throw new UnrecoverableSmslibException("Cannot init pdu object, wrong payload");
+ }
+ pdu.setDataBytes(bytes);
+ } else {
+ String text = getPayload().getText();
+ if (text == null) {
+ throw new UnrecoverableSmslibException("Cannot init pdu object, wrong payload");
+ }
+ pdu.setDecodedText(text);
+ }
+ }
+
+ @Override
+ public String getSignature() {
+ return hashSignature(String.format("%s-%s", getRecipientAddress(), getId()));
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/Payload.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/Payload.java
new file mode 100644
index 0000000000000..b7b0f4a70c2a3
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/message/Payload.java
@@ -0,0 +1,54 @@
+package org.smslib.message;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class Payload {
+ public enum Type {
+ Text,
+ Binary
+ }
+
+ private @Nullable String textData;
+
+ private byte @Nullable [] binaryData;
+
+ private Type type;
+
+ public Payload(String data) {
+ this.type = Type.Text;
+ this.textData = data;
+ }
+
+ public Payload(byte[] data) {
+ this.type = Type.Binary;
+ this.binaryData = data.clone();
+ }
+
+ public Payload(Payload p) {
+ this.type = p.getType();
+ this.textData = (this.type == Type.Text ? p.getText() : "");
+ byte[] bytes = p.getBytes();
+ this.binaryData = (this.type == Type.Binary && bytes != null ? bytes.clone() : null);
+ }
+
+ public Type getType() {
+ return this.type;
+ }
+
+ public @Nullable String getText() {
+ return (this.type == Type.Text ? this.textData : null);
+ }
+
+ public byte @Nullable [] getBytes() {
+ return (this.type == Type.Binary ? this.binaryData : null);
+ }
+
+ public boolean isMultipart() {
+ return false;
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/Pdu.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/Pdu.java
new file mode 100644
index 0000000000000..eb8834db0bd63
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/Pdu.java
@@ -0,0 +1,520 @@
+package org.smslib.pduUtils.gsm3040;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.smslib.UnrecoverableSmslibException;
+import org.smslib.message.MsIsdn;
+import org.smslib.message.MsIsdn.Type;
+import org.smslib.pduUtils.gsm3040.ie.ConcatInformationElement;
+import org.smslib.pduUtils.gsm3040.ie.InformationElement;
+import org.smslib.pduUtils.gsm3040.ie.PortInformationElement;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public abstract class Pdu {
+ // PDU class
+ // this class holds directly usable data only
+ // - all lengths are ints
+ // - dates and strings are already decoded
+ // - byte[] for binary data that can interpreted later
+ // an object of this type is created via a PduParser
+ // or created raw, has its field set and supplied to a PduGenerator
+ // ==================================================
+ // SMSC INFO
+ // ==================================================
+ private int smscInfoLength;
+
+ private int smscAddressType;
+
+ @Nullable
+ private String smscAddress;
+
+ public int getSmscInfoLength() {
+ return this.smscInfoLength;
+ }
+
+ public void setSmscInfoLength(int smscInfoLength) {
+ this.smscInfoLength = smscInfoLength;
+ }
+
+ public void setSmscAddressType(int smscAddressType) {
+ this.smscAddressType = PduUtils.createAddressType(smscAddressType);
+ }
+
+ public int getSmscAddressType() {
+ return this.smscAddressType;
+ }
+
+ public void setSmscAddress(@Nullable String smscAddress) {
+ if (smscAddress == null || "".equals(smscAddress)) {
+ this.smscAddress = null;
+ this.smscAddressType = 0;
+ this.smscInfoLength = 0;
+ return;
+ }
+ // strip the + since it is not needed
+ if (smscAddress.startsWith("+")) {
+ this.smscAddress = smscAddress.substring(1);
+ } else {
+ this.smscAddress = smscAddress;
+ }
+ }
+
+ public @Nullable String getSmscAddress() {
+ return this.smscAddress;
+ }
+
+ // ==================================================
+ // FIRST OCTET
+ // ==================================================
+ private int firstOctet = 0;
+
+ public int getFirstOctet() {
+ return this.firstOctet;
+ }
+
+ public void setFirstOctet(int value) {
+ this.firstOctet = value;
+ }
+
+ protected void setFirstOctetField(int fieldName, int fieldValue, int[] allowedValues) {
+ for (int value : allowedValues) {
+ if (value == fieldValue) {
+ // clear the bits for this field
+ this.firstOctet &= fieldName;
+ // copy the new bits on to it
+ this.firstOctet |= fieldValue;
+ return;
+ }
+ }
+ throw new IllegalArgumentException("Invalid value for fieldName.");
+ }
+
+ protected int getFirstOctetField(int fieldName) {
+ return this.firstOctet & ~fieldName;
+ }
+
+ protected void checkTpMti(int[] allowedTypes) {
+ int tpMti = getTpMti();
+ for (int type : allowedTypes) {
+ if (tpMti == type) {
+ return;
+ }
+ }
+ throw new IllegalArgumentException("Invalid message type : " + getTpMti());
+ }
+
+ public int getTpMti() {
+ return getFirstOctetField(PduUtils.TP_MTI_MASK);
+ }
+
+ public void setTpUdhi(int value) {
+ setFirstOctetField(PduUtils.TP_UDHI_MASK, value,
+ new int[] { PduUtils.TP_UDHI_NO_UDH, PduUtils.TP_UDHI_WITH_UDH });
+ }
+
+ public boolean hasTpUdhi() {
+ return getFirstOctetField(PduUtils.TP_UDHI_MASK) == PduUtils.TP_UDHI_WITH_UDH;
+ }
+
+ // ==================================================
+ // PROTOCOL IDENTIFIER
+ // ==================================================
+ // usually just 0x00 for regular SMS
+ private int protocolIdentifier = 0x00;
+
+ public void setProtocolIdentifier(int protocolIdentifier) {
+ this.protocolIdentifier = protocolIdentifier;
+ }
+
+ public int getProtocolIdentifier() {
+ return this.protocolIdentifier;
+ }
+
+ // ==================================================
+ // DATA CODING SCHEME
+ // ==================================================
+ // usually just 0x00 for default GSM alphabet, phase 2
+ private int dataCodingScheme = 0x00;
+
+ public void setDataCodingScheme(int encoding) {
+ switch (encoding & ~PduUtils.DCS_ENCODING_MASK) {
+ case PduUtils.DCS_ENCODING_7BIT:
+ case PduUtils.DCS_ENCODING_8BIT:
+ case PduUtils.DCS_ENCODING_UCS2:
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid encoding value: " + PduUtils.byteToPdu(encoding));
+ }
+ this.dataCodingScheme = encoding;
+ }
+
+ public int getDataCodingScheme() {
+ return this.dataCodingScheme;
+ }
+
+ // ==================================================
+ // TYPE-OF-ADDRESS
+ // ==================================================
+ private int addressType;
+
+ public int getAddressType() {
+ return this.addressType;
+ }
+
+ public void setAddressType(int addressType) {
+ // insure last bit is always set
+ this.addressType = PduUtils.createAddressType(addressType);
+ }
+
+ // ==================================================
+ // ADDRESS
+ // ==================================================
+ // swapped BCD-format for numbers
+ // 7-bit GSM string for alphanumeric
+ private @Nullable String address;
+
+ public void setAddress(MsIsdn address) {
+ if (address.getType() == Type.Void) {
+ this.address = "";
+ } else {
+ this.address = address.getAddress();
+ }
+ setAddressType(PduUtils.getAddressTypeFor(address));
+ }
+
+ public @Nullable String getAddress() {
+ return this.address;
+ }
+
+ // ==================================================
+ // USER DATA SECTION
+ // ==================================================
+ // this is still needs to be stored since it does not always represent
+ // length in octets, for 7-bit encoding this is length in SEPTETS
+ // NOTE: udData.length may not equal udLength if 7-bit encoding is used
+ private int udLength;
+
+ private byte @Nullable [] udData;
+
+ public int getUDLength() {
+ return this.udLength;
+ }
+
+ public void setUDLength(int udLength) {
+ this.udLength = udLength;
+ }
+
+ public byte @Nullable [] getUDData() {
+ return this.udData;
+ }
+
+ // NOTE: udData DOES NOT include the octet with the length
+ public void setUDData(byte[] udData) {
+ this.udData = udData;
+ }
+
+ // ==================================================
+ // USER DATA HEADER
+ // ==================================================
+ // all methods accessing UDH specific methods require the UDHI to be set
+ // or else an exception will result
+ private static final int UDH_CHECK_MODE_ADD_IF_NONE = 0;
+
+ private static final int UDH_CHECK_MODE_EXCEPTION_IF_NONE = 1;
+
+ private static final int UDH_CHECK_MODE_IGNORE_IF_NONE = 2;
+
+ private void checkForUDHI(int udhCheckMode) {
+ if (!hasTpUdhi()) {
+ switch (udhCheckMode) {
+ case UDH_CHECK_MODE_EXCEPTION_IF_NONE:
+ throw new IllegalStateException("PDU does not have a UDHI in the first octet");
+ case UDH_CHECK_MODE_ADD_IF_NONE:
+ setTpUdhi(PduUtils.TP_UDHI_WITH_UDH);
+ break;
+ case UDH_CHECK_MODE_IGNORE_IF_NONE:
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid UDH check mode");
+ }
+ }
+ }
+
+ public int getTotalUDHLength() {
+ int udhLength = getUDHLength();
+ if (udhLength == 0) {
+ return 0;
+ }
+ // also takes into account the field holding the length
+ // it self
+ return udhLength + 1;
+ }
+
+ public int getUDHLength() {
+ // compute based on the IEs
+ int udhLength = 0;
+ for (InformationElement ie : this.ieMap.values()) {
+ // length + 2 to account for the octet holding the IE length and id
+ udhLength = udhLength + ie.getLength() + 2;
+ }
+ return udhLength;
+ }
+
+ public byte @Nullable [] getUDHData() {
+ checkForUDHI(UDH_CHECK_MODE_IGNORE_IF_NONE);
+ int totalUdhLength = getTotalUDHLength();
+ if (totalUdhLength == 0) {
+ return null;
+ }
+ byte[] retVal = new byte[totalUdhLength];
+ byte[] finalUdData = this.udData;
+ if (finalUdData != null) {
+ System.arraycopy(finalUdData, 0, retVal, 0, totalUdhLength);
+ } else {
+ throw new IllegalArgumentException("Cannot get udhd data because udData is null");
+ }
+ return retVal;
+ }
+
+ // UDH portion of UD if UDHI is present
+ // only Concat and Port info will be treated specially
+ // other IEs will have to get extracted from the map manually and parsed
+ private HashMap ieMap = new HashMap<>();
+
+ private ArrayList ieList = new ArrayList<>();
+
+ public void addInformationElement(InformationElement ie) {
+ checkForUDHI(UDH_CHECK_MODE_ADD_IF_NONE);
+ this.ieMap.put(ie.getIdentifier(), ie);
+ this.ieList.add(ie);
+ }
+
+ public @Nullable InformationElement getInformationElement(int iei) {
+ checkForUDHI(UDH_CHECK_MODE_IGNORE_IF_NONE);
+ return this.ieMap.get(iei);
+ }
+
+ // this is only used in the parser generator
+ public Iterator getInformationElements() {
+ checkForUDHI(UDH_CHECK_MODE_IGNORE_IF_NONE);
+ return this.ieList.iterator();
+ }
+
+ // ==================================================
+ // CONCAT INFO
+ // ==================================================
+ public boolean isConcatMessage() {
+ // check if iei 0x00 or 0x08 is present
+ return (getConcatInfo() != null);
+ }
+
+ public @Nullable ConcatInformationElement getConcatInfo() {
+ checkForUDHI(UDH_CHECK_MODE_IGNORE_IF_NONE);
+ ConcatInformationElement concat = (ConcatInformationElement) getInformationElement(
+ ConcatInformationElement.CONCAT_8BIT_REF);
+ if (concat == null) {
+ concat = (ConcatInformationElement) getInformationElement(ConcatInformationElement.CONCAT_16BIT_REF);
+ }
+ return concat;
+ }
+
+ public int getMpRefNo() {
+ ConcatInformationElement concat = getConcatInfo();
+ if (concat != null) {
+ return concat.getMpRefNo();
+ }
+ return 0;
+ }
+
+ public int getMpMaxNo() {
+ ConcatInformationElement concat = getConcatInfo();
+ if (concat != null) {
+ return concat.getMpMaxNo();
+ }
+ return 1;
+ }
+
+ public int getMpSeqNo() {
+ ConcatInformationElement concat = getConcatInfo();
+ if (concat != null) {
+ return concat.getMpSeqNo();
+ }
+ return 0;
+ }
+
+ // ==================================================
+ // PORT DATA
+ // ==================================================
+ public boolean isPortedMessage() {
+ // check if iei 0x05 is present
+ return (getPortInfo() != null);
+ }
+
+ private @Nullable PortInformationElement getPortInfo() {
+ checkForUDHI(UDH_CHECK_MODE_IGNORE_IF_NONE);
+ return (PortInformationElement) getInformationElement(PortInformationElement.PORT_16BIT);
+ }
+
+ public int getDestPort() {
+ PortInformationElement portIe = getPortInfo();
+ if (portIe == null) {
+ return -1;
+ }
+ return portIe.getDestPort();
+ }
+
+ public int getSrcPort() {
+ PortInformationElement portIe = getPortInfo();
+ if (portIe == null) {
+ return -1;
+ }
+ return portIe.getSrcPort();
+ }
+
+ // ==================================================
+ // NON-UDH DATA
+ // ==================================================
+ // UD minus the UDH portion, same as userData if
+ // no UDH
+ // these fields store data for the generation step
+ private @Nullable String decodedText;
+
+ private byte @Nullable [] dataBytes;
+
+ public void setDataBytes(byte[] dataBytes) {
+ this.dataBytes = dataBytes;
+ this.decodedText = null;
+ // clear the encoding bits for this field 8-bit/data
+ // this.dataCodingScheme &= PduUtils.DCS_ENCODING_MASK;
+ // this.dataCodingScheme |= PduUtils.DCS_ENCODING_8BIT;
+ // this.dataCodingScheme |= PduUtils.DCS_CODING_GROUP_DATA;
+ }
+
+ public byte @Nullable [] getDataBytes() {
+ return this.dataBytes;
+ }
+
+ public boolean isBinary() {
+ // use the DCS coding group or 8bit encoding
+ // Changed following line according to http://code.google.com/p/smslib/issues/detail?id=187
+ // return ((this.dataCodingScheme & PduUtils.DCS_CODING_GROUP_DATA) == PduUtils.DCS_CODING_GROUP_DATA ||
+ // (this.dataCodingScheme & PduUtils.DCS_ENCODING_8BIT) == PduUtils.DCS_ENCODING_8BIT);
+ if ((this.dataCodingScheme & PduUtils.DCS_CODING_GROUP_DATA) == PduUtils.DCS_CODING_GROUP_DATA
+ || (this.dataCodingScheme & PduUtils.DCS_ENCODING_8BIT) == PduUtils.DCS_ENCODING_8BIT) {
+ if ((this.dataCodingScheme & PduUtils.DCS_ENCODING_8BIT) == PduUtils.DCS_ENCODING_8BIT) {
+ return (true);
+ }
+ }
+ return (false);
+ }
+
+ public void setDecodedText(String decodedText) {
+ this.decodedText = decodedText;
+ this.dataBytes = null;
+ // check if existing DCS indicates a flash message
+ boolean flash = false;
+ if (PduUtils.extractDcsFlash(this.dataCodingScheme) == PduUtils.DCS_MESSAGE_CLASS_FLASH) {
+ flash = true;
+ }
+ // clears the coding group to be text again in case it was originally binary
+ this.dataCodingScheme &= PduUtils.DCS_CODING_GROUP_MASK;
+ // set the flash bit back since the above would clear it
+ if (flash) {
+ this.dataCodingScheme = this.dataCodingScheme | PduUtils.DCS_MESSAGE_CLASS_FLASH;
+ }
+ }
+
+ public String getDecodedText() {
+ // this should be try-catched in case the ud data is
+ // actually binary and might cause a decoding exception
+ String decodedTextFinal = this.decodedText;
+ if (decodedTextFinal != null) {
+ return decodedTextFinal;
+ }
+ if (this.udData == null) {
+ throw new UnrecoverableSmslibException("No udData to decode");
+ }
+ return decodeNonUDHDataAsString();
+ }
+
+ public byte[] getUserDataAsBytes() {
+ byte[] udDataFinal = this.udData;
+ if (udDataFinal == null) {
+ throw new UnrecoverableSmslibException("udData cannot be null");
+ }
+ int remainingLength = udDataFinal.length - (getTotalUDHLength());
+ byte[] retVal = new byte[remainingLength];
+ byte[] finalUdData = udData;
+ if (finalUdData != null) {
+ System.arraycopy(finalUdData, getTotalUDHLength(), retVal, 0, remainingLength);
+ } else {
+ throw new UnrecoverableSmslibException("Cannot get user data because udData is null");
+ }
+ return retVal;
+ }
+
+ private String decodeNonUDHDataAsString() {
+ // convert PDU to text depending on the encoding
+ // must also take into account the octet holding the length
+ byte[] udhDataFinal = getUDHData();
+ byte[] udDataFinal = this.udData;
+ if (udDataFinal == null) {
+ throw new UnrecoverableSmslibException("Cannot decode with udData null");
+ }
+ switch (PduUtils.extractDcsEncoding(getDataCodingScheme())) {
+ case PduUtils.DCS_ENCODING_7BIT:
+ // unpack all septets to octets with MSB holes
+ byte[] septets = PduUtils.encodedSeptetsToUnencodedSeptets(udDataFinal);
+ int septetUDHLength = 0;
+ if (udhDataFinal != null) {
+ // work out how much of the UD is UDH
+ septetUDHLength = udhDataFinal.length * 8 / 7;
+ if (udhDataFinal.length * 8 % 7 > 0) {
+ septetUDHLength++;
+ }
+ }
+ byte[] septetsNoUDH = new byte[this.udLength - septetUDHLength];
+ // src, srcStart, dest, destStart, length
+ System.arraycopy(septets, septetUDHLength, septetsNoUDH, 0, septetsNoUDH.length);
+ return PduUtils.unencodedSeptetsToString(septetsNoUDH);
+ case PduUtils.DCS_ENCODING_8BIT:
+ return PduUtils.decode8bitEncoding(udhDataFinal, udDataFinal);
+ case PduUtils.DCS_ENCODING_UCS2:
+ return PduUtils.decodeUcs2Encoding(udhDataFinal, udDataFinal);
+ }
+ throw new IllegalArgumentException("Invalid dataCodingScheme: " + getDataCodingScheme());
+ }
+
+ protected String formatTimestamp(Calendar timestamp) {
+ SimpleDateFormat sdf = new SimpleDateFormat();
+ sdf.applyPattern("EEE dd-MMM-yyyy HH:mm:ss z");
+ sdf.setTimeZone(timestamp.getTimeZone());
+ return sdf.format(timestamp.getTime());
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduFactory.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduFactory.java
new file mode 100644
index 0000000000000..cd6cbfbe2df8e
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduFactory.java
@@ -0,0 +1,66 @@
+package org.smslib.pduUtils.gsm3040;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class PduFactory {
+ public static SmsSubmitPdu newSmsSubmitPdu() {
+ // apply defaults
+ int additionalFields = PduUtils.TP_RD_ACCEPT_DUPLICATES | PduUtils.TP_VPF_INTEGER;
+ return newSmsSubmitPdu(additionalFields);
+ }
+
+ public static SmsSubmitPdu newSmsSubmitPdu(int additionalFields) {
+ // remove any TP_MTI values
+ int firstOctet = PduUtils.TP_MTI_SMS_SUBMIT | additionalFields;
+ return (SmsSubmitPdu) createPdu(firstOctet);
+ }
+
+ private static int getFirstOctetField(int firstOctet, int fieldName) {
+ return firstOctet & ~fieldName;
+ }
+
+ // used to determine what Pdu to use based on the first octet
+ // this is the only way to instantiate a Pdu object
+ public static Pdu createPdu(int firstOctet) {
+ Pdu pdu = null;
+ int messageType = getFirstOctetField(firstOctet, PduUtils.TP_MTI_MASK);
+ switch (messageType) {
+ case PduUtils.TP_MTI_SMS_DELIVER:
+ pdu = new SmsDeliveryPdu();
+ break;
+ case PduUtils.TP_MTI_SMS_STATUS_REPORT:
+ pdu = new SmsStatusReportPdu();
+ break;
+ case PduUtils.TP_MTI_SMS_SUBMIT:
+ pdu = new SmsSubmitPdu();
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid TP-MTI value: " + messageType);
+ }
+ // once set, you can't change it
+ // this method is only available in this package
+ pdu.setFirstOctet(firstOctet);
+ return pdu;
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduGenerator.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduGenerator.java
new file mode 100644
index 0000000000000..77453acf2e8fe
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduGenerator.java
@@ -0,0 +1,545 @@
+package org.smslib.pduUtils.gsm3040;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TimeZone;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.smslib.UnrecoverableSmslibException;
+import org.smslib.pduUtils.gsm3040.ie.ConcatInformationElement;
+import org.smslib.pduUtils.gsm3040.ie.InformationElement;
+import org.smslib.pduUtils.gsm3040.ie.InformationElementFactory;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class PduGenerator {
+ private ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ private int firstOctetPosition = -1;
+
+ private boolean updateFirstOctet = false;
+
+ protected void writeSmscInfo(Pdu pdu) {
+ String smscAddress = pdu.getSmscAddress();
+ if (smscAddress != null) {
+ writeBCDAddress(smscAddress, pdu.getSmscAddressType(), pdu.getSmscInfoLength());
+ } else {
+ writeByte(0);
+ }
+ }
+
+ protected void writeFirstOctet(Pdu pdu) {
+ // store the position in case it will need to be updated later
+ this.firstOctetPosition = pdu.getSmscInfoLength() + 1;
+ writeByte(pdu.getFirstOctet());
+ }
+
+ // validity period conversion from hours to the proper integer
+ protected void writeValidityPeriodInteger(int validityPeriod) {
+ if (validityPeriod == -1) {
+ this.baos.write(0xFF);
+ } else {
+ int validityInt;
+ if (validityPeriod <= 12) {
+ validityInt = (validityPeriod * 12) - 1;
+ } else if (validityPeriod <= 24) {
+ validityInt = (((validityPeriod - 12) * 2) + 143);
+ } else if (validityPeriod <= 720) {
+ validityInt = (validityPeriod / 24) + 166;
+ } else {
+ validityInt = (validityPeriod / 168) + 192;
+ }
+ this.baos.write(validityInt);
+ }
+ }
+
+ protected void writeTimeStampStringForDate(Date timestamp) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(timestamp);
+ int year = cal.get(Calendar.YEAR) - 2000;
+ int month = cal.get(Calendar.MONTH) + 1;
+ int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
+ int hourOfDay = cal.get(Calendar.HOUR_OF_DAY);
+ int minute = cal.get(Calendar.MINUTE);
+ int sec = cal.get(Calendar.SECOND);
+ TimeZone tz = cal.getTimeZone();
+ int offset = tz.getOffset(timestamp.getTime());
+ int minOffset = offset / 60000;
+ int tzValue = minOffset / 15;
+ // for negative offsets, add 128 to the absolute value
+ if (tzValue < 0) {
+ tzValue = 128 - tzValue;
+ }
+ // note: the nibbles are written as BCD style
+ this.baos.write(PduUtils.createSwappedBCD(year));
+ this.baos.write(PduUtils.createSwappedBCD(month));
+ this.baos.write(PduUtils.createSwappedBCD(dayOfMonth));
+ this.baos.write(PduUtils.createSwappedBCD(hourOfDay));
+ this.baos.write(PduUtils.createSwappedBCD(minute));
+ this.baos.write(PduUtils.createSwappedBCD(sec));
+ this.baos.write(PduUtils.createSwappedBCD(tzValue));
+ }
+
+ protected void writeAddress(String address, int addressType, int addressLength) throws IOException {
+ switch (PduUtils.extractAddressType(addressType)) {
+ case PduUtils.ADDRESS_TYPE_ALPHANUMERIC:
+ byte[] textSeptets = PduUtils.stringToUnencodedSeptets(address);
+ byte[] alphaNumBytes = PduUtils.encode7bitUserData(null, textSeptets);
+ // ADDRESS LENGTH - should be the semi-octet count
+ // - this type is not used for SMSCInfo
+ this.baos.write(alphaNumBytes.length * 2);
+ // ADDRESS TYPE
+ this.baos.write(addressType);
+ // ADDRESS TEXT
+ this.baos.write(alphaNumBytes);
+ break;
+ default:
+ // BCD-style
+ writeBCDAddress(address, addressType, addressLength);
+ }
+ }
+
+ protected void writeBCDAddress(String address, int addressType, int addressLength) {
+ // BCD-style
+ // ADDRESS LENGTH - either an octet count or semi-octet count
+ this.baos.write(addressLength);
+ // ADDRESS TYPE
+ this.baos.write(addressType);
+ // ADDRESS NUMBERS
+ // if address.length is not even, pad the string an with F at the end
+ String myaddress = address;
+ if (myaddress.length() % 2 == 1) {
+ myaddress = myaddress + "F";
+ }
+ int digit = 0;
+ for (int i = 0; i < myaddress.length(); i++) {
+ char c = myaddress.charAt(i);
+ if (i % 2 == 1) {
+ digit |= ((Integer.parseInt(Character.toString(c), 16)) << 4);
+ this.baos.write(digit);
+ // clear it
+ digit = 0;
+ } else {
+ digit |= (Integer.parseInt(Character.toString(c), 16) & 0x0F);
+ }
+ }
+ }
+
+ protected void writeUDData(Pdu pdu, int mpRefNo, int partNo) {
+ int dcs = pdu.getDataCodingScheme();
+ try {
+ switch (PduUtils.extractDcsEncoding(dcs)) {
+ case PduUtils.DCS_ENCODING_7BIT:
+ writeUDData7bit(pdu, mpRefNo, partNo);
+ break;
+ case PduUtils.DCS_ENCODING_8BIT:
+ writeUDData8bit(pdu, mpRefNo, partNo);
+ break;
+ case PduUtils.DCS_ENCODING_UCS2:
+ writeUDDataUCS2(pdu, mpRefNo, partNo);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid DCS encoding: " + PduUtils.extractDcsEncoding(dcs));
+ }
+ } catch (IOException e) {
+ throw new UnrecoverableSmslibException("Cannot write uddata", e);
+ }
+ }
+
+ protected void writeUDH(Pdu pdu) throws IOException {
+ // stream directly into the internal baos
+ writeUDH(pdu, this.baos);
+ }
+
+ protected void writeUDH(Pdu pdu, ByteArrayOutputStream udhBaos) throws IOException {
+ // need to insure that proper concat info is inserted
+ // before writing if needed
+ // i.e. the reference number, maxseq and seq have to be set from
+ // outside (OutboundMessage)
+ udhBaos.write(pdu.getUDHLength());
+ for (Iterator ieIterator = pdu.getInformationElements(); ieIterator.hasNext();) {
+ InformationElement ie = ieIterator.next();
+ udhBaos.write(ie.getIdentifier());
+ udhBaos.write(ie.getLength());
+ udhBaos.write(ie.getData());
+ }
+ }
+
+ protected int computeOffset(Pdu pdu, int maxMessageLength, int partNo) {
+ // computes offset to which part of the string is to be encoded into the PDU
+ // also sets the MpMaxNo field of the concatInfo if message is multi-part
+ int offset;
+ int maxParts = 1;
+ if (!pdu.isBinary()) {
+ maxParts = pdu.getDecodedText().length() / maxMessageLength + 1;
+ } else {
+ byte[] pduDataBytes = pdu.getDataBytes();
+ if (pduDataBytes == null) {
+ throw new UnrecoverableSmslibException("Cannot compute offset for empty data bytes");
+ }
+ maxParts = pduDataBytes.length / maxMessageLength + 1;
+ }
+ if (pdu.hasTpUdhi()) {
+ ConcatInformationElement concatInfoFinal = pdu.getConcatInfo();
+ if (concatInfoFinal != null) {
+ if (partNo > 0) {
+ concatInfoFinal.setMpMaxNo(maxParts);
+ }
+ }
+ }
+ if ((maxParts > 1) && (partNo > 0)) {
+ // - if partNo > maxParts
+ // - error
+ if (partNo > maxParts) {
+ throw new IllegalArgumentException("Invalid partNo: " + partNo + ", maxParts=" + maxParts);
+ }
+ offset = ((partNo - 1) * maxMessageLength);
+ } else {
+ // just get from the start
+ offset = 0;
+ }
+ return offset;
+ }
+
+ protected void checkForConcat(Pdu pdu, int lengthOfText, int maxLength, int maxLengthWithUdh, int mpRefNo,
+ int partNo) {
+ if ((lengthOfText <= maxLengthWithUdh) || ((lengthOfText > maxLengthWithUdh) && (lengthOfText <= maxLength))) {
+ } else {
+ // need concat
+ ConcatInformationElement concatInfoFinal = pdu.getConcatInfo();
+ if (concatInfoFinal != null) {
+ // if concatInfo is already present then just replace the values with the supplied
+ concatInfoFinal.setMpRefNo(mpRefNo);
+ concatInfoFinal.setMpSeqNo(partNo);
+ } else {
+ // add concat info with the specified mpRefNo, bogus maxSeqNo, and partNo
+ // bogus maxSeqNo will be replaced once it is known in the later steps
+ // this just needs to be added since its presence is needed to compute
+ // the UDH length
+ ConcatInformationElement concatInfo = InformationElementFactory.generateConcatInfo(mpRefNo, partNo);
+ pdu.addInformationElement(concatInfo);
+ this.updateFirstOctet = true;
+ }
+ }
+ }
+
+ protected int computePotentialUdhLength(Pdu pdu) {
+ int currentUdhLength = pdu.getTotalUDHLength();
+ if (currentUdhLength == 0) {
+ // add 1 for the UDH Length field
+ return ConcatInformationElement.getDefaultConcatLength() + 1;
+ }
+ // this already has the UDH Length field, no need to add 1
+ return currentUdhLength + ConcatInformationElement.getDefaultConcatLength();
+ }
+
+ protected void writeUDData7bit(Pdu pdu, int mpRefNo, int partNo) throws IOException {
+ String decodedText = pdu.getDecodedText();
+ // partNo states what part of the unencoded text will be used
+ // - max length is based on the size of the UDH
+ // for 7bit => maxLength = 160 - total UDH septets
+ // check if this message needs a concat
+ byte[] textSeptetsForDecodedText = PduUtils.stringToUnencodedSeptets(decodedText);
+ int potentialUdhLength = PduUtils.getNumSeptetsForOctets(computePotentialUdhLength(pdu));
+ checkForConcat(pdu, textSeptetsForDecodedText.length,
+ 160 - PduUtils.getNumSeptetsForOctets(pdu.getTotalUDHLength()), // CHANGED
+ 160 - potentialUdhLength, mpRefNo, partNo);
+ // given the IEs in the pdu derive the max message body length
+ // this length will include the potential concat added in the previous step
+ int totalUDHLength = pdu.getTotalUDHLength();
+ int maxMessageLength = 160 - PduUtils.getNumSeptetsForOctets(totalUDHLength);
+ // get septets for part
+ byte[] textSeptets = getUnencodedSeptetsForPart(pdu, maxMessageLength, partNo);
+ // udlength is the sum of udh septet length and the text septet length
+ int udLength = PduUtils.getNumSeptetsForOctets(totalUDHLength) + textSeptets.length;
+ this.baos.write(udLength);
+ // generate UDH byte[]
+ // UDHL (sum of all IE lengths)
+ // IE list
+ byte[] udhBytes = null;
+ if (pdu.hasTpUdhi()) {
+ ByteArrayOutputStream udhBaos = new ByteArrayOutputStream();
+ writeUDH(pdu, udhBaos);
+ // buffer the udh since this needs to be 7-bit encoded with the text
+ udhBytes = udhBaos.toByteArray();
+ }
+ // encode both as one unit
+ byte[] udBytes = PduUtils.encode7bitUserData(udhBytes, textSeptets);
+ // write combined encoded array
+ this.baos.write(udBytes);
+ }
+
+ private byte[] getUnencodedSeptetsForPart(Pdu pdu, int maxMessageLength, int partNo) {
+ // computes offset to which part of the string is to be encoded into the PDU
+ // also sets the MpMaxNo field of the concatInfo if message is multi-part
+ int offset;
+ int maxParts = 1;
+ // must use the unencoded septets not the actual string since
+ // it is possible that some special characters in string are multi-septet
+ byte[] unencodedSeptets = PduUtils.stringToUnencodedSeptets(pdu.getDecodedText());
+ maxParts = (unencodedSeptets.length / maxMessageLength) + 1;
+ if (pdu.hasTpUdhi()) {
+ ConcatInformationElement concatInfoFinal = pdu.getConcatInfo();
+ if (concatInfoFinal != null) {
+ if (partNo > 0) {
+ concatInfoFinal.setMpMaxNo(maxParts);
+ }
+ }
+ }
+ if ((maxParts > 1) && (partNo > 0)) {
+ // - if partNo > maxParts
+ // - error
+ if (partNo > maxParts) {
+ throw new UnrecoverableSmslibException("Invalid partNo: " + partNo + ", maxParts=" + maxParts);
+ }
+ offset = ((partNo - 1) * maxMessageLength);
+ } else {
+ // just get from the start
+ offset = 0;
+ }
+ // copy the portion of the full unencoded septet array for this part
+ byte[] septetsForPart = new byte[Math.min(maxMessageLength, unencodedSeptets.length - offset)];
+ System.arraycopy(unencodedSeptets, offset, septetsForPart, 0, septetsForPart.length);
+ return septetsForPart;
+ }
+
+ protected void writeUDData8bit(Pdu pdu, int mpRefNo, int partNo) throws IOException {
+ // NOTE: binary messages are also handled here
+ byte[] data;
+ if (pdu.isBinary()) {
+ // use the supplied bytes
+ byte[] dataBytesFinal = pdu.getDataBytes();
+ if (dataBytesFinal == null) {
+ throw new UnrecoverableSmslibException("Data cannot be null");
+ }
+ data = dataBytesFinal;
+ } else {
+ // encode the text
+ data = PduUtils.encode8bitUserData(pdu.getDecodedText());
+ }
+ // partNo states what part of the unencoded text will be used
+ // - max length is based on the size of the UDH
+ // for 8bit => maxLength = 140 - the total UDH bytes
+ // check if this message needs a concat
+ int potentialUdhLength = computePotentialUdhLength(pdu);
+ checkForConcat(pdu, data.length, 140 - pdu.getTotalUDHLength(), // CHANGED
+ 140 - potentialUdhLength, mpRefNo, partNo);
+ // given the IEs in the pdu derive the max message body length
+ // this length will include the potential concat added in the previous step
+ int totalUDHLength = pdu.getTotalUDHLength();
+ int maxMessageLength = 140 - totalUDHLength;
+ // compute which portion of the message will be part of the message
+ int offset = computeOffset(pdu, maxMessageLength, partNo);
+ byte[] dataToWrite = new byte[Math.min(maxMessageLength, data.length - offset)];
+ System.arraycopy(data, offset, dataToWrite, 0, dataToWrite.length);
+ // generate udlength
+ // based on partNo
+ // udLength is an octet count for 8bit/ucs2
+ int udLength = totalUDHLength + dataToWrite.length;
+ // write udlength
+ this.baos.write(udLength);
+ // write UDH to the stream directly
+ if (pdu.hasTpUdhi()) {
+ writeUDH(pdu, this.baos);
+ }
+ // write data
+ this.baos.write(dataToWrite);
+ }
+
+ protected void writeUDDataUCS2(Pdu pdu, int mpRefNo, int partNo) throws IOException {
+ String decodedText = pdu.getDecodedText();
+ // partNo states what part of the unencoded text will be used
+ // - max length is based on the size of the UDH
+ // for ucs2 => maxLength = (140 - the total UDH bytes)/2
+ // check if this message needs a concat
+ int potentialUdhLength = computePotentialUdhLength(pdu);
+ checkForConcat(pdu, decodedText.length(), (140 - pdu.getTotalUDHLength()) / 2, // CHANGED
+ (140 - potentialUdhLength) / 2, mpRefNo, partNo);
+ // given the IEs in the pdu derive the max message body length
+ // this length will include the potential concat added in the previous step
+ int totalUDHLength = pdu.getTotalUDHLength();
+ int maxMessageLength = (140 - totalUDHLength) / 2;
+ // compute which portion of the message will be part of the message
+ int offset = computeOffset(pdu, maxMessageLength, partNo);
+ String textToEncode = decodedText.substring(offset, Math.min(offset + maxMessageLength, decodedText.length()));
+ // generate udlength
+ // based on partNo
+ // udLength is an octet count for 8bit/ucs2
+ int udLength = totalUDHLength + (textToEncode.length() * 2);
+ // write udlength
+ this.baos.write(udLength);
+ // write UDH to the stream directly
+ if (pdu.hasTpUdhi()) {
+ writeUDH(pdu, this.baos);
+ }
+ // write encoded text
+ this.baos.write(PduUtils.encodeUcs2UserData(textToEncode));
+ }
+
+ protected void writeByte(int i) {
+ this.baos.write(i);
+ }
+
+ protected void writeBytes(byte[] b) throws IOException {
+ this.baos.write(b);
+ }
+
+ public List generatePduList(Pdu pdu, int mpRefNo) {
+ // generate all required PDUs for a given message
+ // mpRefNo comes from the ModemGateway
+ ArrayList pduList = new ArrayList<>();
+ for (int i = 1; i <= pdu.getMpMaxNo(); i++) {
+ String pduString = generatePduString(pdu, mpRefNo, i);
+ pduList.add(pduString);
+ }
+ return pduList;
+ }
+
+ // NOTE: partNo indicates which part of a multipart message to generate
+ // assuming that the message is multipart, this will be ignored if the
+ // message is not a concat message
+ public String generatePduString(Pdu pdu, int mpRefNo, int partNo) {
+ try {
+ this.baos = new ByteArrayOutputStream();
+ this.firstOctetPosition = -1;
+ this.updateFirstOctet = false;
+ // process the PDU
+ switch (pdu.getTpMti()) {
+ case PduUtils.TP_MTI_SMS_DELIVER:
+ generateSmsDeliverPduString((SmsDeliveryPdu) pdu, mpRefNo, partNo);
+ break;
+ case PduUtils.TP_MTI_SMS_SUBMIT:
+ generateSmsSubmitPduString((SmsSubmitPdu) pdu, mpRefNo, partNo);
+ break;
+ case PduUtils.TP_MTI_SMS_STATUS_REPORT:
+ generateSmsStatusReportPduString((SmsStatusReportPdu) pdu);
+ break;
+ }
+ // in case concat is detected in the writeUD() method
+ // and there was no UDHI at the time of detection
+ // the old firstOctet must be overwritten with the new value
+ byte[] pduBytes = this.baos.toByteArray();
+ if (this.updateFirstOctet) {
+ pduBytes[this.firstOctetPosition] = (byte) (pdu.getFirstOctet() & 0xFF);
+ }
+ return PduUtils.bytesToPdu(pduBytes);
+ } catch (IOException e) {
+ throw new UnrecoverableSmslibException("Cannot generate pdu", e);
+ }
+ }
+
+ protected void generateSmsSubmitPduString(SmsSubmitPdu pdu, int mpRefNo, int partNo) throws IOException {
+ String address = pdu.getAddress();
+ if (address == null) {
+ throw new IllegalArgumentException("adress cannot be null");
+ }
+ // SMSC address info
+ writeSmscInfo(pdu);
+ // first octet
+ writeFirstOctet(pdu);
+ // message reference
+ writeByte(pdu.getMessageReference());
+ // destination address info
+ writeAddress(address, pdu.getAddressType(), address.length());
+ // protocol id
+ writeByte(pdu.getProtocolIdentifier());
+ // data coding scheme
+ writeByte(pdu.getDataCodingScheme());
+ // validity period
+ switch (pdu.getTpVpf()) {
+ case PduUtils.TP_VPF_INTEGER:
+ writeValidityPeriodInteger(pdu.getValidityPeriod());
+ break;
+ case PduUtils.TP_VPF_TIMESTAMP:
+ Date validityDate = pdu.getValidityDate();
+ if (validityDate == null) {
+ throw new IllegalArgumentException("Cannot get validity date for pdu");
+ }
+ writeTimeStampStringForDate(validityDate);
+ break;
+ }
+ // user data
+ // headers
+ writeUDData(pdu, mpRefNo, partNo);
+ }
+
+ // NOTE: the following are just for validation of the PduParser
+ // - there is no normal scenario where these are used
+ protected void generateSmsDeliverPduString(SmsDeliveryPdu pdu, int mpRefNo, int partNo) throws IOException {
+ // SMSC address info
+ writeSmscInfo(pdu);
+ // first octet
+ writeFirstOctet(pdu);
+ // originator address info
+ String address = pdu.getAddress();
+ if (address == null) {
+ throw new IllegalArgumentException("Address cannot be null");
+ }
+ writeAddress(address, pdu.getAddressType(), address.length());
+ // protocol id
+ writeByte(pdu.getProtocolIdentifier());
+ // data coding scheme
+ writeByte(pdu.getDataCodingScheme());
+ // timestamp
+ Date timestamp = pdu.getTimestamp();
+ if (timestamp != null) {
+ writeTimeStampStringForDate(timestamp);
+ }
+ // user data
+ // headers
+ writeUDData(pdu, mpRefNo, partNo);
+ }
+
+ protected void generateSmsStatusReportPduString(SmsStatusReportPdu pdu) throws IOException {
+ // SMSC address info
+ writeSmscInfo(pdu);
+ // first octet
+ writeFirstOctet(pdu);
+ // message reference
+ writeByte(pdu.getMessageReference());
+ // destination address info
+ String address = pdu.getAddress();
+ if (address == null) {
+ throw new IllegalArgumentException("Address cannot be null");
+ }
+ writeAddress(address, pdu.getAddressType(), address.length());
+ // timestamp
+ Date timestamp = pdu.getTimestamp();
+ if (timestamp == null) {
+ throw new IllegalArgumentException("cannot write null timestamp");
+ }
+ writeTimeStampStringForDate(timestamp);
+ // discharge time(timestamp)
+ Date dischargeTime = pdu.getDischargeTime();
+ if (dischargeTime == null) {
+ throw new IllegalArgumentException("cannot write null dischargeTime");
+ }
+ writeTimeStampStringForDate(dischargeTime);
+ // status
+ writeByte(pdu.getStatus());
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduParser.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduParser.java
new file mode 100644
index 0000000000000..33d97474232de
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduParser.java
@@ -0,0 +1,343 @@
+package org.smslib.pduUtils.gsm3040;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.smslib.UnrecoverableSmslibException;
+import org.smslib.message.MsIsdn;
+import org.smslib.pduUtils.gsm3040.ie.InformationElement;
+import org.smslib.pduUtils.gsm3040.ie.InformationElementFactory;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class PduParser {
+ // ==================================================
+ // RAW PDU PARSER
+ // ==================================================
+ // increments as methods are called
+ private int position;
+
+ private byte @Nullable [] pduByteArray;
+
+ // possible types of data
+ // BCD digits
+ // byte
+ // gsm-septets
+ // timestamp info
+ private int readByte() {
+ // read 8-bits forward
+ byte[] pduByteArrayFinal = this.pduByteArray;
+ if (pduByteArrayFinal == null) {
+ throw new UnrecoverableSmslibException("Cannot read byte from null data");
+ }
+ int retVal = pduByteArrayFinal[this.position] & 0xFF;
+ this.position++;
+ return retVal;
+ }
+
+ private int readSwappedNibbleBCDByte() {
+ // read 8-bits forward, swap the nibbles
+ int data = readByte();
+ data = PduUtils.swapNibbles((byte) data);
+ int retVal = 0;
+ retVal += ((data >>> 4) & 0xF) * 10;
+ retVal += ((data & 0xF));
+ return retVal;
+ }
+
+ private Calendar readTimeStamp() {
+ // reads timestamp info
+ // 7 bytes in semi-octet(BCD) style
+ int year = readSwappedNibbleBCDByte();
+ int month = readSwappedNibbleBCDByte();
+ int day = readSwappedNibbleBCDByte();
+ int hour = readSwappedNibbleBCDByte();
+ int minute = readSwappedNibbleBCDByte();
+ int second = readSwappedNibbleBCDByte();
+ // special treatment for timezone due to sign bit
+ // swap nibbles, clear the sign bit, convert remaining bits to BCD
+ int timestamp = readByte();
+ boolean negative = (timestamp & 0x08) == 0x08; // check bit 3
+ int timezone = PduUtils.swapNibbles(timestamp) & 0x7F; // remove last bit since this is just a sign
+ // time zone computation
+ TimeZone tz = null;
+ if (negative) {
+ // bit 3 of unswapped value represents the sign (1 == negative, 0 == positive)
+ // when swapped this will now be bit 7 (128)
+ int bcdTimeZone = 0;
+ bcdTimeZone += (((timezone >>> 4) & 0xF) * 10);
+ bcdTimeZone += ((timezone & 0xF));
+ timezone = bcdTimeZone;
+ int totalMinutes = timezone * 15;
+ int hours = totalMinutes / 60;
+ int minutes = totalMinutes % 60;
+ String gmtString = "GMT-" + hours + ":" + (minutes < 10 ? "0" : "") + minutes;
+ // System.out.println(gmtString);
+ tz = TimeZone.getTimeZone(gmtString);
+ } else {
+ int bcdTimeZone = 0;
+ bcdTimeZone += ((timezone >>> 4) & 0xF) * 10;
+ bcdTimeZone += ((timezone & 0xF));
+ timezone = bcdTimeZone;
+ int totalMinutes = timezone * 15;
+ int hours = totalMinutes / 60;
+ int minutes = totalMinutes % 60;
+ String gmtString = "GMT+" + hours + ":" + (minutes < 10 ? "0" : "") + minutes;
+ // System.out.println(gmtString);
+ tz = TimeZone.getTimeZone(gmtString);
+ }
+ Calendar cal = Calendar.getInstance(tz);
+ cal.set(Calendar.YEAR, year + 2000);
+ cal.set(Calendar.MONTH, month - 1);
+ cal.set(Calendar.DAY_OF_MONTH, day);
+ cal.set(Calendar.HOUR_OF_DAY, hour);
+ cal.set(Calendar.MINUTE, minute);
+ cal.set(Calendar.SECOND, second);
+ return cal;
+ }
+
+ private @Nullable String readAddress(int addressLength, int addressType) {
+ // NOTE: the max number of octets on an address is 12 octets
+ // this means that an address field need only be 12 octets long
+ // what about for 7-bit? This would be 13 chars at 12 octets?
+ if (addressLength > 0) {
+ // length is a semi-octet count
+ int addressDataOctetLength = addressLength / 2 + ((addressLength % 2 == 1) ? 1 : 0);
+ // extract data and increment position
+ byte[] addressData = new byte[addressDataOctetLength];
+ byte[] pduByteArrayFinal = this.pduByteArray;
+ if (pduByteArrayFinal != null) {
+ System.arraycopy(pduByteArrayFinal, this.position, addressData, 0, addressDataOctetLength);
+ } else {
+ throw new UnrecoverableSmslibException("Cannot read address because pdu data is null");
+ }
+ this.position = this.position + addressDataOctetLength;
+ switch (PduUtils.extractAddressType(addressType)) {
+ case PduUtils.ADDRESS_TYPE_ALPHANUMERIC:
+ // extract and process encoded bytes
+ byte[] uncompressed = PduUtils.encodedSeptetsToUnencodedSeptets(addressData);
+ int septets = addressLength * 4 / 7;
+ byte[] choppedAddressData = new byte[septets];
+ System.arraycopy(uncompressed, 0, choppedAddressData, 0, septets);
+ return PduUtils.unencodedSeptetsToString(choppedAddressData);
+ default:
+ // process BCD style data any other
+ return PduUtils.readBCDNumbers(addressLength, addressData);
+ }
+ }
+ return null;
+ }
+
+ private int readValidityPeriodInt() {
+ // this will convert the VP to #MINUTES
+ int validity = readByte();
+ int minutes = 0;
+ if ((validity > 0) && (validity <= 143)) {
+ // groups of 5 min
+ minutes = (validity + 1) * 5;
+ } else if ((validity > 143) && (validity <= 167)) {
+ // groups of 30 min + 12 hrs
+ minutes = (12 * 60) + (validity - 143) * 30;
+ } else if ((validity > 167) && (validity <= 196)) {
+ // days
+ minutes = (validity - 166) * 24 * 60;
+ } else if ((validity > 197) && (validity <= 255)) {
+ // weeks
+ minutes = (validity - 192) * 7 * 24 * 60;
+ }
+ return minutes;
+ }
+
+ public Pdu parsePdu(String rawPdu) {
+ // encode pdu to byte[] for easier processing
+ this.pduByteArray = PduUtils.pduToBytes(rawPdu);
+ this.position = 0;
+ // parse start and determine what type of pdu it is
+ Pdu pdu = parseStart();
+ // parse depending on the pdu type
+ switch (pdu.getTpMti()) {
+ case PduUtils.TP_MTI_SMS_DELIVER:
+ parseSmsDeliverMessage((SmsDeliveryPdu) pdu);
+ break;
+ case PduUtils.TP_MTI_SMS_SUBMIT:
+ parseSmsSubmitMessage((SmsSubmitPdu) pdu);
+ break;
+ case PduUtils.TP_MTI_SMS_STATUS_REPORT:
+ parseSmsStatusReportMessage((SmsStatusReportPdu) pdu);
+ break;
+ }
+ return pdu;
+ }
+
+ private Pdu parseStart() {
+ // SMSC info
+ // length
+ // address type
+ // smsc data
+ int addressLength = readByte();
+ Pdu pdu = null;
+ if (addressLength > 0) {
+ int addressType = readByte();
+ String smscAddress = readAddress((addressLength - 1) * 2, addressType);
+ // first octet - determine how to parse and how to store
+ int firstOctet = readByte();
+ pdu = PduFactory.createPdu(firstOctet);
+ // generic methods
+ pdu.setSmscAddressType(addressType);
+ pdu.setSmscAddress(smscAddress);
+ pdu.setSmscInfoLength(addressLength);
+ } else {
+ // first octet - determine how to parse and how to store
+ int firstOctet = readByte();
+ pdu = PduFactory.createPdu(firstOctet);
+ }
+ return pdu;
+ }
+
+ private void parseUserData(Pdu pdu) {
+ // ud length
+ // NOTE: - the udLength value is just stored, it is not used to determine the length
+ // of the remaining data (it may be a septet length not an octet length)
+ // - parser just assumes that the remaining PDU data is for the User-Data field
+ int udLength = readByte();
+ pdu.setUDLength(udLength);
+ // user data
+ // NOTE: UD Data does not contain the length octet
+ byte[] pduByteArrayFinal = this.pduByteArray;
+ if (pduByteArrayFinal != null) {
+ int udOctetLength = pduByteArrayFinal.length - this.position;
+ byte[] udData = new byte[udOctetLength];
+ System.arraycopy(pduByteArrayFinal, this.position, udData, 0, udOctetLength);
+ // save the UD data
+ pdu.setUDData(udData);
+ } else {
+ throw new UnrecoverableSmslibException("Cannot parse user data because pdu data is null");
+ }
+ // user data header (if present)
+ // position is still at the start of the UD
+ if (pdu.hasTpUdhi()) {
+ // udh length
+ int udhLength = readByte();
+ // udh data (iterate till udh is consumed)
+ // iei id
+ // iei data length
+ // iei data
+ int endUdh = this.position + udhLength;
+ while (this.position < endUdh) {
+ int iei = readByte();
+ int iedl = readByte();
+ byte[] ieData = new byte[iedl];
+ System.arraycopy(pduByteArrayFinal, this.position, ieData, 0, iedl);
+ InformationElement ie = InformationElementFactory.createInformationElement(iei, ieData);
+ pdu.addInformationElement(ie);
+ this.position = this.position + iedl;
+ if (this.position > endUdh) {
+ // at the end, position after adding should be exactly at endUdh
+ throw new UnrecoverableSmslibException(
+ "UDH is shorter than expected endUdh=" + endUdh + ", position=" + this.position);
+ }
+ }
+ }
+ }
+
+ private void parseSmsDeliverMessage(SmsDeliveryPdu pdu) {
+ // originator address info
+ // address length
+ // type of address
+ // address data
+ int addressLength = readByte();
+ int addressType = readByte();
+ String originatorAddress = readAddress(addressLength, addressType);
+ pdu.setAddressType(addressType);
+ if (originatorAddress != null) {
+ pdu.setAddress(new MsIsdn(originatorAddress));
+ }
+ // protocol id
+ int protocolId = readByte();
+ pdu.setProtocolIdentifier(protocolId);
+ // data coding scheme
+ int dcs = readByte();
+ pdu.setDataCodingScheme(dcs);
+ // timestamp
+ Calendar timestamp = readTimeStamp();
+ pdu.setTimestamp(timestamp);
+ // user data
+ parseUserData(pdu);
+ }
+
+ private void parseSmsStatusReportMessage(SmsStatusReportPdu pdu) {
+ // message reference
+ int messageReference = readByte();
+ pdu.setMessageReference(messageReference);
+ // destination address info
+ int addressLength = readByte();
+ int addressType = readByte();
+ String destinationAddress = readAddress(addressLength, addressType);
+ pdu.setAddressType(addressType);
+ pdu.setAddress(new MsIsdn(destinationAddress));
+ // timestamp
+ Calendar timestamp = readTimeStamp();
+ pdu.setTimestamp(timestamp);
+ // discharge time(timestamp)
+ Calendar timestamp2 = readTimeStamp();
+ pdu.setDischargeTime(timestamp2);
+ // status
+ int status = readByte();
+ pdu.setStatus(status);
+ }
+
+ // NOTE: the following is just for validation of the PduGenerator
+ // - there is no normal scenario where this is used
+ private void parseSmsSubmitMessage(SmsSubmitPdu pdu) {
+ // message reference
+ int messageReference = readByte();
+ pdu.setMessageReference(messageReference);
+ // destination address info
+ int addressLength = readByte();
+ int addressType = readByte();
+ String destinationAddress = readAddress(addressLength, addressType);
+ pdu.setAddressType(addressType);
+ pdu.setAddress(new MsIsdn(destinationAddress));
+ // protocol id
+ int protocolId = readByte();
+ pdu.setProtocolIdentifier(protocolId);
+ // data coding scheme
+ int dcs = readByte();
+ pdu.setDataCodingScheme(dcs);
+ // validity period
+ switch (pdu.getTpVpf()) {
+ case PduUtils.TP_VPF_NONE:
+ break;
+ case PduUtils.TP_VPF_INTEGER:
+ int validityInt = readValidityPeriodInt();
+ pdu.setValidityPeriod(validityInt / 60); // pdu assumes hours
+ break;
+ case PduUtils.TP_VPF_TIMESTAMP:
+ Calendar validityDate = readTimeStamp();
+ pdu.setValidityTimestamp(validityDate);
+ break;
+ }
+ parseUserData(pdu);
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduUtils.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduUtils.java
new file mode 100644
index 0000000000000..e4f09752493b6
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/PduUtils.java
@@ -0,0 +1,761 @@
+package org.smslib.pduUtils.gsm3040;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.BitSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.smslib.UnrecoverableSmslibException;
+import org.smslib.message.MsIsdn;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class PduUtils {
+ // ==================================================
+ // GSM ALPHABET
+ // ==================================================
+ private static final char[][] grcAlphabetRemapping = { { '\u0386', '\u0041' }, // GREEK CAPITAL LETTER ALPHA WITH
+ // TONOS
+ { '\u0388', '\u0045' }, // GREEK CAPITAL LETTER EPSILON WITH TONOS
+ { '\u0389', '\u0048' }, // GREEK CAPITAL LETTER ETA WITH TONOS
+ { '\u038A', '\u0049' }, // GREEK CAPITAL LETTER IOTA WITH TONOS
+ { '\u038C', '\u004F' }, // GREEK CAPITAL LETTER OMICRON WITH TONOS
+ { '\u038E', '\u0059' }, // GREEK CAPITAL LETTER UPSILON WITH TONOS
+ { '\u038F', '\u03A9' }, // GREEK CAPITAL LETTER OMEGA WITH TONOS
+ { '\u0390', '\u0049' }, // GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS
+ { '\u0391', '\u0041' }, // GREEK CAPITAL LETTER ALPHA
+ { '\u0392', '\u0042' }, // GREEK CAPITAL LETTER BETA
+ { '\u0393', '\u0393' }, // GREEK CAPITAL LETTER GAMMA
+ { '\u0394', '\u0394' }, // GREEK CAPITAL LETTER DELTA
+ { '\u0395', '\u0045' }, // GREEK CAPITAL LETTER EPSILON
+ { '\u0396', '\u005A' }, // GREEK CAPITAL LETTER ZETA
+ { '\u0397', '\u0048' }, // GREEK CAPITAL LETTER ETA
+ { '\u0398', '\u0398' }, // GREEK CAPITAL LETTER THETA
+ { '\u0399', '\u0049' }, // GREEK CAPITAL LETTER IOTA
+ { '\u039A', '\u004B' }, // GREEK CAPITAL LETTER KAPPA
+ { '\u039B', '\u039B' }, // GREEK CAPITAL LETTER LAMDA
+ { '\u039C', '\u004D' }, // GREEK CAPITAL LETTER MU
+ { '\u039D', '\u004E' }, // GREEK CAPITAL LETTER NU
+ { '\u039E', '\u039E' }, // GREEK CAPITAL LETTER XI
+ { '\u039F', '\u004F' }, // GREEK CAPITAL LETTER OMICRON
+ { '\u03A0', '\u03A0' }, // GREEK CAPITAL LETTER PI
+ { '\u03A1', '\u0050' }, // GREEK CAPITAL LETTER RHO
+ { '\u03A3', '\u03A3' }, // GREEK CAPITAL LETTER SIGMA
+ { '\u03A4', '\u0054' }, // GREEK CAPITAL LETTER TAU
+ { '\u03A5', '\u0059' }, // GREEK CAPITAL LETTER UPSILON
+ { '\u03A6', '\u03A6' }, // GREEK CAPITAL LETTER PHI
+ { '\u03A7', '\u0058' }, // GREEK CAPITAL LETTER CHI
+ { '\u03A8', '\u03A8' }, // GREEK CAPITAL LETTER PSI
+ { '\u03A9', '\u03A9' }, // GREEK CAPITAL LETTER OMEGA
+ { '\u03AA', '\u0049' }, // GREEK CAPITAL LETTER IOTA WITH DIALYTIKA
+ { '\u03AB', '\u0059' }, // GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA
+ { '\u03AC', '\u0041' }, // GREEK SMALL LETTER ALPHA WITH TONOS
+ { '\u03AD', '\u0045' }, // GREEK SMALL LETTER EPSILON WITH TONOS
+ { '\u03AE', '\u0048' }, // GREEK SMALL LETTER ETA WITH TONOS
+ { '\u03AF', '\u0049' }, // GREEK SMALL LETTER IOTA WITH TONOS
+ { '\u03B0', '\u0059' }, // GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS
+ { '\u03B1', '\u0041' }, // GREEK SMALL LETTER ALPHA
+ { '\u03B2', '\u0042' }, // GREEK SMALL LETTER BETA
+ { '\u03B3', '\u0393' }, // GREEK SMALL LETTER GAMMA
+ { '\u03B4', '\u0394' }, // GREEK SMALL LETTER DELTA
+ { '\u03B5', '\u0045' }, // GREEK SMALL LETTER EPSILON
+ { '\u03B6', '\u005A' }, // GREEK SMALL LETTER ZETA
+ { '\u03B7', '\u0048' }, // GREEK SMALL LETTER ETA
+ { '\u03B8', '\u0398' }, // GREEK SMALL LETTER THETA
+ { '\u03B9', '\u0049' }, // GREEK SMALL LETTER IOTA
+ { '\u03BA', '\u004B' }, // GREEK SMALL LETTER KAPPA
+ { '\u03BB', '\u039B' }, // GREEK SMALL LETTER LAMDA
+ { '\u03BC', '\u004D' }, // GREEK SMALL LETTER MU
+ { '\u03BD', '\u004E' }, // GREEK SMALL LETTER NU
+ { '\u03BE', '\u039E' }, // GREEK SMALL LETTER XI
+ { '\u03BF', '\u004F' }, // GREEK SMALL LETTER OMICRON
+ { '\u03C0', '\u03A0' }, // GREEK SMALL LETTER PI
+ { '\u03C1', '\u0050' }, // GREEK SMALL LETTER RHO
+ { '\u03C2', '\u03A3' }, // GREEK SMALL LETTER FINAL SIGMA
+ { '\u03C3', '\u03A3' }, // GREEK SMALL LETTER SIGMA
+ { '\u03C4', '\u0054' }, // GREEK SMALL LETTER TAU
+ { '\u03C5', '\u0059' }, // GREEK SMALL LETTER UPSILON
+ { '\u03C6', '\u03A6' }, // GREEK SMALL LETTER PHI
+ { '\u03C7', '\u0058' }, // GREEK SMALL LETTER CHI
+ { '\u03C8', '\u03A8' }, // GREEK SMALL LETTER PSI
+ { '\u03C9', '\u03A9' }, // GREEK SMALL LETTER OMEGA
+ { '\u03CA', '\u0049' }, // GREEK SMALL LETTER IOTA WITH DIALYTIKA
+ { '\u03CB', '\u0059' }, // GREEK SMALL LETTER UPSILON WITH DIALYTIKA
+ { '\u03CC', '\u004F' }, // GREEK SMALL LETTER OMICRON WITH TONOS
+ { '\u03CD', '\u0059' }, // GREEK SMALL LETTER UPSILON WITH TONOS
+ { '\u03CE', '\u03A9' } // GREEK SMALL LETTER OMEGA WITH TONOS
+ };
+
+ private static final char[] extAlphabet = { '\u000c', // FORM FEED
+ '\u005e', // CIRCUMFLEX ACCENT
+ '\u007b', // LEFT CURLY BRACKET
+ '\u007d', // RIGHT CURLY BRACKET
+ '\\', // REVERSE SOLIDUS
+ '\u005b', // LEFT SQUARE BRACKET
+ '\u007e', // TILDE
+ '\u005d', // RIGHT SQUARE BRACKET
+ '\u007c', // VERTICAL LINES
+ '\u20ac', // EURO SIGN
+ };
+
+ private static final String[] extBytes = { "1b0a", // FORM FEED
+ "1b14", // CIRCUMFLEX ACCENT
+ "1b28", // LEFT CURLY BRACKET
+ "1b29", // RIGHT CURLY BRACKET
+ "1b2f", // REVERSE SOLIDUS
+ "1b3c", // LEFT SQUARE BRACKET
+ "1b3d", // TILDE
+ "1b3e", // RIGHT SQUARE BRACKET
+ "1b40", // VERTICAL LINES
+ "1b65", // EURO SIGN
+ };
+
+ // NOTE: this is an adjustment required to compensate for
+ // multi-byte characters split across the end of a pdu part
+ // if the previous part is noted to be ending in a '1b'
+ // call this method on the first char of the next part
+ // to adjust it for the missing '1b'
+ public static String getMultiCharFor(char c) {
+ switch (c) {
+ // GSM 0x0A (line feed) ==> form feed
+ case '\n':
+ return "'\u000c'";
+ // GSM 0x14 (greek capital lamda) ==> circumflex
+ case '\u039B':
+ return "^";
+ // GSM 0x28 (left parenthesis) ==> left curly brace
+ case '(':
+ return "{";
+ // GSM 0x29 (right parenthesis) ==> right curly brace
+ case ')':
+ return "}";
+ // GSM 0x2f (solidus or slash) ==> reverse solidus or backslash
+ case '/':
+ return "\\";
+ // GSM 0x3c (less than sign) ==> left square bracket
+ case '<':
+ return "[";
+ // GSM 0x3d (equals sign) ==> tilde
+ case '=':
+ return "~";
+ // GSM 0x3e (greater than sign) ==> right square bracket
+ case '>':
+ return "]";
+ // GSM 0x40 (inverted exclamation point) ==> pipe
+ case '\u00A1':
+ return "|";
+ // GSM 0x65 (latin small e) ==> euro
+ case 'e':
+ return "\u20ac";
+ }
+ return "";
+ }
+
+ private static final char[] stdAlphabet = { '\u0040', // COMMERCIAL AT
+ '\u00A3', // POUND SIGN
+ '\u0024', // DOLLAR SIGN
+ '\u00A5', // YEN SIGN
+ '\u00E8', // LATIN SMALL LETTER E WITH GRAVE
+ '\u00E9', // LATIN SMALL LETTER E WITH ACUTE
+ '\u00F9', // LATIN SMALL LETTER U WITH GRAVE
+ '\u00EC', // LATIN SMALL LETTER I WITH GRAVE
+ '\u00F2', // LATIN SMALL LETTER O WITH GRAVE
+ '\u00E7', // LATIN SMALL LETTER C WITH CEDILLA
+ '\n', // LINE FEED
+ '\u00D8', // LATIN CAPITAL LETTER O WITH STROKE
+ '\u00F8', // LATIN SMALL LETTER O WITH STROKE
+ '\r', // CARRIAGE RETURN
+ '\u00C5', // LATIN CAPITAL LETTER A WITH RING ABOVE
+ '\u00E5', // LATIN SMALL LETTER A WITH RING ABOVE
+ '\u0394', // GREEK CAPITAL LETTER DELTA
+ '\u005F', // LOW LINE
+ '\u03A6', // GREEK CAPITAL LETTER PHI
+ '\u0393', // GREEK CAPITAL LETTER GAMMA
+ '\u039B', // GREEK CAPITAL LETTER LAMDA
+ '\u03A9', // GREEK CAPITAL LETTER OMEGA
+ '\u03A0', // GREEK CAPITAL LETTER PI
+ '\u03A8', // GREEK CAPITAL LETTER PSI
+ '\u03A3', // GREEK CAPITAL LETTER SIGMA
+ '\u0398', // GREEK CAPITAL LETTER THETA
+ '\u039E', // GREEK CAPITAL LETTER XI
+ '\u00A0', // ESCAPE TO EXTENSION TABLE (or displayed as NBSP, see
+ // note
+ // above)
+ '\u00C6', // LATIN CAPITAL LETTER AE
+ '\u00E6', // LATIN SMALL LETTER AE
+ '\u00DF', // LATIN SMALL LETTER SHARP S (German)
+ '\u00C9', // LATIN CAPITAL LETTER E WITH ACUTE
+ '\u0020', // SPACE
+ '\u0021', // EXCLAMATION MARK
+ '\u0022', // QUOTATION MARK
+ '\u0023', // NUMBER SIGN
+ '\u00A4', // CURRENCY SIGN
+ '\u0025', // PERCENT SIGN
+ '\u0026', // AMPERSAND
+ '\'', // APOSTROPHE
+ '\u0028', // LEFT PARENTHESIS
+ '\u0029', // RIGHT PARENTHESIS
+ '\u002A', // ASTERISK
+ '\u002B', // PLUS SIGN
+ '\u002C', // COMMA
+ '\u002D', // HYPHEN-MINUS
+ '\u002E', // FULL STOP
+ '\u002F', // SOLIDUS
+ '\u0030', // DIGIT ZERO
+ '\u0031', // DIGIT ONE
+ '\u0032', // DIGIT TWO
+ '\u0033', // DIGIT THREE
+ '\u0034', // DIGIT FOUR
+ '\u0035', // DIGIT FIVE
+ '\u0036', // DIGIT SIX
+ '\u0037', // DIGIT SEVEN
+ '\u0038', // DIGIT EIGHT
+ '\u0039', // DIGIT NINE
+ '\u003A', // COLON
+ '\u003B', // SEMICOLON
+ '\u003C', // LESS-THAN SIGN
+ '\u003D', // EQUALS SIGN
+ '\u003E', // GREATER-THAN SIGN
+ '\u003F', // QUESTION MARK
+ '\u00A1', // INVERTED EXCLAMATION MARK
+ '\u0041', // LATIN CAPITAL LETTER A
+ '\u0042', // LATIN CAPITAL LETTER B
+ '\u0043', // LATIN CAPITAL LETTER C
+ '\u0044', // LATIN CAPITAL LETTER D
+ '\u0045', // LATIN CAPITAL LETTER E
+ '\u0046', // LATIN CAPITAL LETTER F
+ '\u0047', // LATIN CAPITAL LETTER G
+ '\u0048', // LATIN CAPITAL LETTER H
+ '\u0049', // LATIN CAPITAL LETTER I
+ '\u004A', // LATIN CAPITAL LETTER J
+ '\u004B', // LATIN CAPITAL LETTER K
+ '\u004C', // LATIN CAPITAL LETTER L
+ '\u004D', // LATIN CAPITAL LETTER M
+ '\u004E', // LATIN CAPITAL LETTER N
+ '\u004F', // LATIN CAPITAL LETTER O
+ '\u0050', // LATIN CAPITAL LETTER P
+ '\u0051', // LATIN CAPITAL LETTER Q
+ '\u0052', // LATIN CAPITAL LETTER R
+ '\u0053', // LATIN CAPITAL LETTER S
+ '\u0054', // LATIN CAPITAL LETTER T
+ '\u0055', // LATIN CAPITAL LETTER U
+ '\u0056', // LATIN CAPITAL LETTER V
+ '\u0057', // LATIN CAPITAL LETTER W
+ '\u0058', // LATIN CAPITAL LETTER X
+ '\u0059', // LATIN CAPITAL LETTER Y
+ '\u005A', // LATIN CAPITAL LETTER Z
+ '\u00C4', // LATIN CAPITAL LETTER A WITH DIAERESIS
+ '\u00D6', // LATIN CAPITAL LETTER O WITH DIAERESIS
+ '\u00D1', // LATIN CAPITAL LETTER N WITH TILDE
+ '\u00DC', // LATIN CAPITAL LETTER U WITH DIAERESIS
+ '\u00A7', // SECTION SIGN
+ '\u00BF', // INVERTED QUESTION MARK
+ '\u0061', // LATIN SMALL LETTER A
+ '\u0062', // LATIN SMALL LETTER B
+ '\u0063', // LATIN SMALL LETTER C
+ '\u0064', // LATIN SMALL LETTER D
+ '\u0065', // LATIN SMALL LETTER E
+ '\u0066', // LATIN SMALL LETTER F
+ '\u0067', // LATIN SMALL LETTER G
+ '\u0068', // LATIN SMALL LETTER H
+ '\u0069', // LATIN SMALL LETTER I
+ '\u006A', // LATIN SMALL LETTER J
+ '\u006B', // LATIN SMALL LETTER K
+ '\u006C', // LATIN SMALL LETTER L
+ '\u006D', // LATIN SMALL LETTER M
+ '\u006E', // LATIN SMALL LETTER N
+ '\u006F', // LATIN SMALL LETTER O
+ '\u0070', // LATIN SMALL LETTER P
+ '\u0071', // LATIN SMALL LETTER Q
+ '\u0072', // LATIN SMALL LETTER R
+ '\u0073', // LATIN SMALL LETTER S
+ '\u0074', // LATIN SMALL LETTER T
+ '\u0075', // LATIN SMALL LETTER U
+ '\u0076', // LATIN SMALL LETTER V
+ '\u0077', // LATIN SMALL LETTER W
+ '\u0078', // LATIN SMALL LETTER X
+ '\u0079', // LATIN SMALL LETTER Y
+ '\u007A', // LATIN SMALL LETTER Z
+ '\u00E4', // LATIN SMALL LETTER A WITH DIAERESIS
+ '\u00F6', // LATIN SMALL LETTER O WITH DIAERESIS
+ '\u00F1', // LATIN SMALL LETTER N WITH TILDE
+ '\u00FC', // LATIN SMALL LETTER U WITH DIAERESIS
+ '\u00E0', // LATIN SMALL LETTER A WITH GRAVE
+ };
+
+ // ==================================================
+ // FIRST OCTET CONSTANTS
+ // ==================================================
+ // to add, use the & with MASK to clear bits on original value
+ // and | this cleared value with constant specified
+ // TP-MTI xxxxxx00 = SMS-DELIVER
+ // xxxxxx10 = SMS-STATUS-REPORT
+ // xxxxxx01 = SMS-SUBMIT
+ public static final int TP_MTI_MASK = 0xFC;
+
+ public static final int TP_MTI_SMS_DELIVER = 0x00;
+
+ public static final int TP_MTI_SMS_SUBMIT = 0x01;
+
+ public static final int TP_MTI_SMS_STATUS_REPORT = 0x02;
+
+ // TP-RD xxxxx0xx = accept duplicate messages
+ // xxxxx1xx = reject duplicate messages
+ // for SMS-SUBMIT only
+ public static final int TP_RD_ACCEPT_DUPLICATES = 0x00;
+
+ // TP-VPF xxx00xxx = no validity period
+ // xxx10xxx = validity period integer-representation
+ // xxx11xxx = validity period timestamp-representation
+ public static final int TP_VPF_MASK = 0xE7;
+
+ public static final int TP_VPF_NONE = 0x00;
+
+ public static final int TP_VPF_INTEGER = 0x10;
+
+ public static final int TP_VPF_TIMESTAMP = 0x18;
+
+ // TP-SRI xx0xxxxx = no status report to SME (for SMS-DELIVER only)
+ // xx1xxxxx = status report to SME
+ public static final int TP_SRI_MASK = 0xDF;
+
+ // TP-SRR xx0xxxxx = no status report (for SMS-SUBMIT only)
+ // xx1xxxxx = status report
+
+ public static final int TP_SRR_NO_REPORT = 0x00;
+
+ public static final int TP_SRR_REPORT = 0x20;
+
+ // TP-UDHI x0xxxxxx = no UDH
+ // x1xxxxxx = UDH present
+ public static final int TP_UDHI_MASK = 0xBF;
+
+ public static final int TP_UDHI_NO_UDH = 0x00;
+
+ public static final int TP_UDHI_WITH_UDH = 0x40;
+
+ // ==================================================
+ // ADDRESS-TYPE CONSTANTS
+ // ==================================================
+ // some typical ones used for sending, though receiving may get other types
+ // usually 1 001 0001 (0x91) international format
+ // 1 000 0001 (0x81) (unknown) short number (e.g. access codes)
+ // 1 101 0000 (0xD0) alphanumeric (e.g. access code names like PasaLoad)
+ public static final int ADDRESS_NUMBER_PLAN_ID_TELEPHONE = 0x01;
+
+ public static final int ADDRESS_TYPE_MASK = 0x70;
+
+ public static final int ADDRESS_TYPE_UNKNOWN = 0x00;
+
+ public static final int ADDRESS_TYPE_INTERNATIONAL = 0x10;
+
+ public static final int ADDRESS_TYPE_NATIONAL = 0x20;
+
+ public static final int ADDRESS_TYPE_ALPHANUMERIC = 0x50;
+
+ public static int getAddressTypeFor(MsIsdn number) {
+ switch (number.getType()) {
+ case International:
+ return createAddressType(ADDRESS_TYPE_INTERNATIONAL | ADDRESS_NUMBER_PLAN_ID_TELEPHONE);
+ case National:
+ return createAddressType(ADDRESS_TYPE_NATIONAL | ADDRESS_NUMBER_PLAN_ID_TELEPHONE);
+ default:
+ return createAddressType(ADDRESS_TYPE_UNKNOWN | ADDRESS_NUMBER_PLAN_ID_TELEPHONE);
+ }
+ }
+
+ public static int extractAddressType(int addressType) {
+ return addressType & ADDRESS_TYPE_MASK;
+ }
+
+ public static int createAddressType(int addressType) {
+ // last bit is always set
+ return 0x80 | addressType;
+ }
+
+ // ==================================================
+ // DCS ENCODING CONSTANTS
+ // ==================================================
+ public static final int DCS_CODING_GROUP_MASK = 0x0F;
+
+ public static final int DCS_CODING_GROUP_DATA = 0xF0;
+
+ public static final int DCS_CODING_GROUP_GENERAL = 0xC0;
+
+ public static final int DCS_ENCODING_MASK = 0xF3;
+
+ public static final int DCS_ENCODING_7BIT = 0x00;
+
+ public static final int DCS_ENCODING_8BIT = 0x04;
+
+ public static final int DCS_ENCODING_UCS2 = 0x08;
+
+ public static final int DCS_MESSAGE_CLASS_MASK = 0xEC;
+
+ public static final int DCS_MESSAGE_CLASS_FLASH = 0x10;
+
+ public static final int DCS_MESSAGE_CLASS_ME = 0x11;
+
+ public static final int DCS_MESSAGE_CLASS_SIM = 0x12;
+
+ public static final int DCS_MESSAGE_CLASS_TE = 0x13;
+
+ public static int extractDcsEncoding(int dataCodingScheme) {
+ return dataCodingScheme & ~PduUtils.DCS_ENCODING_MASK;
+ }
+
+ public static int extractDcsClass(int dataCodingScheme) {
+ return dataCodingScheme & ~DCS_MESSAGE_CLASS_MASK;
+ }
+
+ public static int extractDcsFlash(int dataCodingScheme) {
+ // this is only useful if DCS != 0
+ return dataCodingScheme & ~DCS_MESSAGE_CLASS_MASK;
+ }
+
+ public static String decodeDataCodingScheme(Pdu pdu) {
+ StringBuffer sb = new StringBuffer();
+ switch (PduUtils.extractDcsEncoding(pdu.getDataCodingScheme())) {
+ case PduUtils.DCS_ENCODING_7BIT:
+ sb.append("7-bit GSM Alphabet");
+ break;
+ case PduUtils.DCS_ENCODING_8BIT:
+ sb.append("8-bit encoding");
+ break;
+ case PduUtils.DCS_ENCODING_UCS2:
+ sb.append("UCS2 encoding");
+ break;
+ }
+ // are flash messages are only applicable to general coding group?
+ if ((pdu.getDataCodingScheme() & ~PduUtils.DCS_CODING_GROUP_GENERAL) == 0) {
+ switch (PduUtils.extractDcsClass(pdu.getDataCodingScheme())) {
+ case PduUtils.DCS_MESSAGE_CLASS_FLASH:
+ sb.append(", (Flash Message)");
+ break;
+ case PduUtils.DCS_MESSAGE_CLASS_ME:
+ sb.append(", (Class1 ME Message)");
+ break;
+ case PduUtils.DCS_MESSAGE_CLASS_SIM:
+ sb.append(", (Class2 SIM Message)");
+ break;
+ case PduUtils.DCS_MESSAGE_CLASS_TE:
+ sb.append(", (Class3 TE Message)");
+ break;
+ }
+ }
+ return sb.toString();
+ }
+
+ public static byte[] encode8bitUserData(String text) {
+ try {
+ return text.getBytes("ISO8859_1");
+ } catch (UnsupportedEncodingException e) {
+ throw new UnrecoverableSmslibException("Cannot encode user data", e);
+ }
+ }
+
+ public static byte[] encodeUcs2UserData(String text) {
+ try {
+ // UTF-16 Big-Endian, no Byte Order Marker at start
+ return text.getBytes("UTF-16BE");
+ } catch (UnsupportedEncodingException e) {
+ throw new UnrecoverableSmslibException("Cannot encode user data", e);
+ }
+ }
+
+ public static byte[] encode7bitUserData(byte @Nullable [] udhOctets, byte[] textSeptets) {
+ // UDH octets and text have to be encoded together in a single pass
+ // UDH octets will need to be converted to unencoded septets in order
+ // to properly pad the data
+ if (udhOctets == null) {
+ // convert string to uncompressed septets
+ return unencodedSeptetsToEncodedSeptets(textSeptets);
+ }
+ // convert UDH octets as if they were encoded septets
+ // NOTE: DO NOT DISCARD THE LAST SEPTET IF IT IS ZERO
+ byte[] udhSeptets = PduUtils.encodedSeptetsToUnencodedSeptets(udhOctets, false);
+ // combine the two arrays and encode them as a whole
+ byte[] combined = new byte[udhSeptets.length + textSeptets.length];
+ System.arraycopy(udhSeptets, 0, combined, 0, udhSeptets.length);
+ System.arraycopy(textSeptets, 0, combined, udhSeptets.length, textSeptets.length);
+ // convert encoded byte[] to a PDU string
+ return unencodedSeptetsToEncodedSeptets(combined);
+ }
+
+ public static String decode8bitEncoding(byte @Nullable [] udhData, byte[] pduData) {
+ // standard 8-bit characters
+ try {
+ int udhLength = ((udhData == null) ? 0 : udhData.length);
+ return new String(pduData, udhLength, pduData.length - udhLength, "ISO8859_1");
+ } catch (UnsupportedEncodingException e) {
+ throw new UnrecoverableSmslibException("Cannot decode user data", e);
+ }
+ }
+
+ public static String decodeUcs2Encoding(byte @Nullable [] udhData, byte[] pduData) {
+ try {
+ int udhLength = ((udhData == null) ? 0 : udhData.length);
+ // standard unicode
+ return new String(pduData, udhLength, pduData.length - udhLength, "UTF-16");
+ } catch (UnsupportedEncodingException e) {
+ throw new UnrecoverableSmslibException("Cannot decode user data", e);
+ }
+ }
+
+ public static byte swapNibbles(int b) {
+ return (byte) (((b << 4) & 0xF0) | ((b >>> 4) & 0x0F));
+ }
+
+ public static String readBCDNumbers(int numDigits, byte[] addressData) {
+ // reads length BCD numbers from the current position
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < addressData.length; i++) {
+ int b = addressData[i];
+ int num1 = b & 0x0F;
+ sb.append(num1);
+ int num2 = (b >>> 4) & 0x0F;
+ if (num2 != 0x0F) {
+ // check if fillbits
+ sb.append(num2);
+ }
+ }
+ return sb.toString();
+ }
+
+ public static int createSwappedBCD(int decimal) {
+ // creates a swapped BCD representation of a 2-digit decimal
+ int tens = (decimal & 0xFF) / 10;
+ int ones = (decimal & 0xFF) - (tens * 10);
+ return (ones << 4) | tens;
+ }
+
+ // from Java String to uncompressed septets (GSM characters)
+ public static byte[] stringToUnencodedSeptets(String s) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ int i, j, index;
+ char ch;
+ String myS = s;
+ myS = myS.replace('\u00C7', // LATIN CAPITAL LETTER C WITH CEDILLA
+ '\u00E7' // LATIN SMALL LETTER C WITH CEDILLA
+ );
+ for (i = 0; i < myS.length(); i++) {
+ ch = myS.charAt(i);
+ index = -1;
+ for (j = 0; j < extAlphabet.length; j++) {
+ if (extAlphabet[j] == ch) {
+ index = j;
+ break;
+ }
+ }
+ if (index != -1) // An extended char...
+ {
+ baos.write((byte) Integer.parseInt(extBytes[index].substring(0, 2), 16));
+ baos.write((byte) Integer.parseInt(extBytes[index].substring(2, 4), 16));
+ } else
+ // Maybe a standard char...
+ {
+ index = -1;
+ for (j = 0; j < stdAlphabet.length; j++) {
+ if (stdAlphabet[j] == ch) {
+ index = j;
+ baos.write((byte) j);
+ break;
+ }
+ }
+ if (index == -1) // Maybe a Greek Char...
+ {
+ for (j = 0; j < grcAlphabetRemapping.length; j++) {
+ if (grcAlphabetRemapping[j][0] == ch) {
+ index = j;
+ ch = grcAlphabetRemapping[j][1];
+ break;
+ }
+ }
+ if (index != -1) {
+ for (j = 0; j < stdAlphabet.length; j++) {
+ if (stdAlphabet[j] == ch) {
+ index = j;
+ baos.write((byte) j);
+ break;
+ }
+ }
+ } else
+ // Unknown char replacement...
+ {
+ baos.write((byte) ' ');
+ }
+ }
+ }
+ }
+ return baos.toByteArray();
+ }
+
+ // from compress unencoded septets
+ public static byte[] unencodedSeptetsToEncodedSeptets(byte[] septetBytes) {
+ byte[] txtBytes;
+ byte[] txtSeptets;
+ int txtBytesLen;
+ BitSet bits;
+ int i, j;
+ txtBytes = septetBytes;
+ txtBytesLen = txtBytes.length;
+ bits = new BitSet();
+ for (i = 0; i < txtBytesLen; i++) {
+ for (j = 0; j < 7; j++) {
+ if ((txtBytes[i] & (1 << j)) != 0) {
+ bits.set((i * 7) + j);
+ }
+ }
+ }
+ // big diff here
+ int encodedSeptetByteArrayLength = txtBytesLen * 7 / 8 + ((txtBytesLen * 7 % 8 != 0) ? 1 : 0);
+ txtSeptets = new byte[encodedSeptetByteArrayLength];
+ for (i = 0; i < encodedSeptetByteArrayLength; i++) {
+ for (j = 0; j < 8; j++) {
+ txtSeptets[i] |= (byte) ((bits.get((i * 8) + j) ? 1 : 0) << j);
+ }
+ }
+ return txtSeptets;
+ }
+
+ // from GSM characters to java string
+ public static String unencodedSeptetsToString(byte[] bytes) {
+ StringBuffer text;
+ String extChar;
+ int i, j;
+ text = new StringBuffer();
+ for (i = 0; i < bytes.length; i++) {
+ if (bytes[i] == 0x1b) {
+ // NOTE: - ++i can be a problem if the '1b'
+ // is right at the end of a PDU
+ // - this will be an issue for displaying
+ // partial PDUs e.g. via toString()
+ if (i < bytes.length - 1) {
+ extChar = "1b" + Integer.toHexString(bytes[++i]);
+ for (j = 0; j < extBytes.length; j++) {
+ if (extBytes[j].equalsIgnoreCase(extChar)) {
+ text.append(extAlphabet[j]);
+ }
+ }
+ }
+ } else {
+ text.append(stdAlphabet[bytes[i]]);
+ }
+ }
+ return text.toString();
+ }
+
+ public static int getNumSeptetsForOctets(int numOctets) {
+ return numOctets * 8 / 7 + ((numOctets * 8 % 7 != 0) ? 1 : 0);
+ // return numOctets + (numOctets/7);
+ }
+
+ // decompress encoded septets to unencoded form
+ public static byte[] encodedSeptetsToUnencodedSeptets(byte[] octetBytes) {
+ return encodedSeptetsToUnencodedSeptets(octetBytes, true);
+ }
+
+ public static byte[] encodedSeptetsToUnencodedSeptets(byte[] octetBytes, boolean discardLast) {
+ byte newBytes[];
+ BitSet bitSet;
+ int i, j, value1, value2;
+ bitSet = new BitSet(octetBytes.length * 8);
+ value1 = 0;
+ for (i = 0; i < octetBytes.length; i++) {
+ for (j = 0; j < 8; j++) {
+ value1 = (i * 8) + j;
+ if ((octetBytes[i] & (1 << j)) != 0) {
+ bitSet.set(value1);
+ }
+ }
+ }
+ value1++;
+ // this is a bit count NOT a byte count
+ value2 = value1 / 7 + ((value1 % 7 != 0) ? 1 : 0); // big diff here
+ // System.out.println(octetBytes.length);
+ // System.out.println(value1+" --> "+value2);
+ if (value2 == 0) {
+ value2++;
+ }
+ newBytes = new byte[value2];
+ for (i = 0; i < value2; i++) {
+ for (j = 0; j < 7; j++) {
+ if ((value1 + 1) > (i * 7 + j)) {
+ if (bitSet.get(i * 7 + j)) {
+ newBytes[i] |= (byte) (1 << j);
+ }
+ }
+ }
+ }
+ if (discardLast && octetBytes.length * 8 % 7 > 0) {
+ // when decoding a 7bit encoded string
+ // the last septet may become 0, this should be discarded
+ // since this is an artifact of the encoding not part of the
+ // original string
+ // this is only done for decoding 7bit encoded text NOT for
+ // reversing octets to septets (e.g. for the encoding the UDH)
+ if (newBytes[newBytes.length - 1] == 0) {
+ byte[] retVal = new byte[newBytes.length - 1];
+ System.arraycopy(newBytes, 0, retVal, 0, retVal.length);
+ return retVal;
+ }
+ }
+ return newBytes;
+ }
+
+ // converts a PDU style string to a byte array
+ public static byte[] pduToBytes(String s) {
+ byte[] bytes = new byte[s.length() / 2];
+ for (int i = 0; i < s.length(); i += 2) {
+ bytes[i / 2] = (byte) (Integer.parseInt(s.substring(i, i + 2), 16));
+ }
+ return bytes;
+ }
+
+ // converts a byte array to PDU style string
+ public static String bytesToPdu(byte[] bytes) {
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < bytes.length; i++) {
+ sb.append(byteToPdu(bytes[i] & 0xFF));
+ }
+ return sb.toString();
+ }
+
+ public static String byteToBits(byte b) {
+ String bits = Integer.toBinaryString(b & 0xFF);
+ while (bits.length() < 8) {
+ bits = "0" + bits;
+ }
+ return bits;
+ }
+
+ public static String byteToPdu(int b) {
+ StringBuffer sb = new StringBuffer();
+ String s = Integer.toHexString(b & 0xFF);
+ if (s.length() == 1) {
+ sb.append("0");
+ }
+ sb.append(s);
+ return sb.toString().toUpperCase();
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/SmsDeliveryPdu.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/SmsDeliveryPdu.java
new file mode 100644
index 0000000000000..10e421e21da3f
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/SmsDeliveryPdu.java
@@ -0,0 +1,48 @@
+package org.smslib.pduUtils.gsm3040;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class SmsDeliveryPdu extends Pdu {
+ // can only create via the factory
+ SmsDeliveryPdu() {
+ }
+
+ // ==================================================
+ // TIMESTAMP
+ // ==================================================
+ private @Nullable Calendar timestamp;
+
+ public void setTimestamp(Calendar timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public @Nullable Date getTimestamp() {
+ Calendar timestampFinal = this.timestamp;
+ return timestampFinal == null ? null : timestampFinal.getTime();
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/SmsStatusReportPdu.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/SmsStatusReportPdu.java
new file mode 100644
index 0000000000000..8c8ca057da7f0
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/SmsStatusReportPdu.java
@@ -0,0 +1,89 @@
+package org.smslib.pduUtils.gsm3040;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class SmsStatusReportPdu extends Pdu {
+ // can only create via the factory
+ SmsStatusReportPdu() {
+ }
+
+ // ==================================================
+ // MESSAGE REFERENCE
+ // ==================================================
+ // usually just 0x00 to let MC supply
+ private int messageReference = 0x00;
+
+ public void setMessageReference(int reference) {
+ this.messageReference = reference;
+ }
+
+ public int getMessageReference() {
+ return this.messageReference;
+ }
+
+ // ==================================================
+ // STATUS
+ // ==================================================
+ private int status = 0x00;
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public int getStatus() {
+ return this.status;
+ }
+
+ // ==================================================
+ // TIMESTAMP
+ // ==================================================
+ private @Nullable Calendar timestamp;
+
+ public void setTimestamp(Calendar timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public @Nullable Date getTimestamp() {
+ Calendar timestampFinal = this.timestamp;
+ return timestampFinal == null ? null : timestampFinal.getTime();
+ }
+
+ // ==================================================
+ // DISCHARGE TIME
+ // ==================================================
+ private @Nullable Calendar dischargeTime;
+
+ public void setDischargeTime(Calendar myDischargeTime) {
+ this.dischargeTime = myDischargeTime;
+ }
+
+ public @Nullable Date getDischargeTime() {
+ Calendar dischargeTimeFinal = this.dischargeTime;
+ return dischargeTimeFinal == null ? null : dischargeTimeFinal.getTime();
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/SmsSubmitPdu.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/SmsSubmitPdu.java
new file mode 100644
index 0000000000000..55c482b06aa43
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/SmsSubmitPdu.java
@@ -0,0 +1,78 @@
+package org.smslib.pduUtils.gsm3040;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class SmsSubmitPdu extends Pdu {
+ // ==================================================
+ // FIRST OCTET UTILITIES
+ // ==================================================
+
+ public int getTpVpf() {
+ return getFirstOctetField(PduUtils.TP_VPF_MASK);
+ }
+
+ // ==================================================
+ // MESSAGE REFERENCE
+ // ==================================================
+ // usually just 0x00 to let MC supply
+ private int messageReference = 0x00;
+
+ public void setMessageReference(int reference) {
+ this.messageReference = reference;
+ }
+
+ public int getMessageReference() {
+ return this.messageReference;
+ }
+
+ // ==================================================
+ // VALIDITY PERIOD
+ // ==================================================
+ // which one is used depends of validity period format (TP-VPF)
+ private int validityPeriod = -1;
+
+ @Nullable
+ private Calendar validityPeriodTimeStamp;
+
+ public int getValidityPeriod() {
+ return this.validityPeriod;
+ }
+
+ public void setValidityPeriod(int validityPeriod) {
+ this.validityPeriod = validityPeriod;
+ }
+
+ public void setValidityTimestamp(Calendar date) {
+ this.validityPeriodTimeStamp = date;
+ }
+
+ public @Nullable Date getValidityDate() {
+ Calendar validityPeriodTimeStampFinal = this.validityPeriodTimeStamp;
+ return validityPeriodTimeStampFinal == null ? null : validityPeriodTimeStampFinal.getTime();
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/ConcatInformationElement.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/ConcatInformationElement.java
new file mode 100644
index 0000000000000..a5f1d98318bde
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/ConcatInformationElement.java
@@ -0,0 +1,188 @@
+package org.smslib.pduUtils.gsm3040.ie;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.smslib.UnrecoverableSmslibException;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class ConcatInformationElement extends InformationElement {
+
+ private static final int CONCAT_IE_LENGTH_8BIT = 5;
+
+ public static final int CONCAT_8BIT_REF = 0x00;
+
+ public static final int CONCAT_16BIT_REF = 0x08;
+
+ private static int defaultConcatType = CONCAT_8BIT_REF;
+
+ private static int defaultConcatLength = CONCAT_IE_LENGTH_8BIT;
+
+ public static int getDefaultConcatLength() {
+ return defaultConcatLength;
+ }
+
+ public static int getDefaultConcatType() {
+ return defaultConcatType;
+ }
+
+ ConcatInformationElement(byte identifier, byte[] data) {
+ super(identifier, data);
+ if (getIdentifier() == CONCAT_8BIT_REF) {
+ // iei
+ // iel
+ // ref
+ // max
+ // seq
+ if (data.length != 3) {
+ throw new IllegalArgumentException("Invalid data length in: " + getClass().getSimpleName());
+ }
+ } else if (getIdentifier() == CONCAT_16BIT_REF) {
+ // iei
+ // iel
+ // ref(2 bytes)
+ // max
+ // seq
+ if (data.length != 4) {
+ throw new IllegalArgumentException("Invalid data length in: " + getClass().getSimpleName());
+ }
+ } else {
+ throw new IllegalArgumentException("Invalid identifier in data in: " + getClass().getSimpleName());
+ }
+ validate();
+ }
+
+ ConcatInformationElement(int identifier, int mpRefNo, int mpMaxNo, int mpSeqNo) {
+ super((byte) (identifier & 0xFF), getData(identifier, mpRefNo, mpMaxNo, mpSeqNo));
+ validate();
+ }
+
+ private static byte[] getData(int identifier, int mpRefNo, int mpMaxNo, int mpSeqNo) {
+ byte[] data = null;
+ switch (identifier) {
+ case CONCAT_8BIT_REF:
+ data = new byte[3];
+ data[0] = (byte) (mpRefNo & 0xFF);
+ data[1] = (byte) (mpMaxNo & 0xFF);
+ data[2] = (byte) (mpSeqNo & 0xFF);
+ break;
+ case CONCAT_16BIT_REF:
+ data = new byte[4];
+ data[0] = (byte) ((mpRefNo & 0xFF00) >>> 8);
+ data[1] = (byte) (mpRefNo & 0xFF);
+ data[2] = (byte) (mpMaxNo & 0xFF);
+ data[3] = (byte) (mpSeqNo & 0xFF);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid identifier for ConcatInformationElement");
+ }
+ return data;
+ }
+
+ public int getMpRefNo() {
+ // this is 8-bit in 0x00 and 16-bit in 0x08
+ byte[] data = getData();
+ if (getIdentifier() == CONCAT_8BIT_REF) {
+ return (data[0] & (0xFF));
+ } else if (getIdentifier() == CONCAT_16BIT_REF) {
+ return ((data[0] << 8) | data[1]) & (0xFFFF);
+ }
+ throw new UnrecoverableSmslibException("Invalid identifier");
+ }
+
+ public void setMpRefNo(int mpRefNo) {
+ // this is 8-bit in 0x00 and 16-bit in 0x08
+ byte[] data = getData();
+ if (getIdentifier() == CONCAT_8BIT_REF) {
+ data[0] = (byte) (mpRefNo & (0xFF));
+ } else if (getIdentifier() == CONCAT_16BIT_REF) {
+ data[0] = (byte) ((mpRefNo >>> 8) & (0xFF));
+ data[1] = (byte) ((mpRefNo) & (0xFF));
+ } else {
+ throw new UnrecoverableSmslibException("Invalid identifier");
+ }
+ }
+
+ public int getMpMaxNo() {
+ byte[] data = getData();
+ if (getIdentifier() == CONCAT_8BIT_REF) {
+ return (data[1] & (0xFF));
+ } else if (getIdentifier() == CONCAT_16BIT_REF) {
+ return (data[2] & (0xFF));
+ }
+ throw new UnrecoverableSmslibException("Invalid identifier");
+ }
+
+ public void setMpMaxNo(int mpMaxNo) {
+ byte[] data = getData();
+ if (getIdentifier() == CONCAT_8BIT_REF) {
+ data[1] = (byte) (mpMaxNo & 0xFF);
+ } else if (getIdentifier() == CONCAT_16BIT_REF) {
+ data[2] = (byte) (mpMaxNo & 0xFF);
+ } else {
+ throw new UnrecoverableSmslibException("Invalid identifier");
+ }
+ }
+
+ public int getMpSeqNo() {
+ byte[] data = getData();
+ if (getIdentifier() == CONCAT_8BIT_REF) {
+ return (data[2] & (0xFF));
+ } else if (getIdentifier() == CONCAT_16BIT_REF) {
+ return (data[3] & (0xFF));
+ }
+ throw new UnrecoverableSmslibException("Invalid identifier");
+ }
+
+ public void setMpSeqNo(int mpSeqNo) {
+ byte[] data = getData();
+ if (getIdentifier() == CONCAT_8BIT_REF) {
+ data[2] = (byte) (mpSeqNo & (0xFF));
+ } else if (getIdentifier() == CONCAT_16BIT_REF) {
+ data[3] = (byte) (mpSeqNo & (0xFF));
+ } else {
+ throw new UnrecoverableSmslibException("Invalid identifier");
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append(super.toString());
+ sb.append("[MpRefNo: ");
+ sb.append(getMpRefNo());
+ sb.append(", MpMaxNo: ");
+ sb.append(getMpMaxNo());
+ sb.append(", MpSeqNo: ");
+ sb.append(getMpSeqNo());
+ sb.append("]");
+ return sb.toString();
+ }
+
+ private void validate() {
+ if (getMpMaxNo() == 0) {
+ throw new IllegalArgumentException("mpMaxNo must be > 0");
+ }
+ if (getMpSeqNo() == 0) {
+ throw new IllegalArgumentException("mpSeqNo must be > 0");
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/InformationElement.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/InformationElement.java
new file mode 100644
index 0000000000000..fd6b1b6645ae1
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/InformationElement.java
@@ -0,0 +1,70 @@
+package org.smslib.pduUtils.gsm3040.ie;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.smslib.pduUtils.gsm3040.PduUtils;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class InformationElement {
+ private byte identifier;
+
+ private byte[] data;
+
+ // iei
+ // iel (implicit length of data)
+ // ied (raw ie data)
+ InformationElement(byte id, byte[] ieData) {
+ this.identifier = id;
+ this.data = ieData;
+ }
+
+ // for outgoing messages
+ void initialize(byte id, byte[] ieData) {
+ this.identifier = id;
+ this.data = ieData;
+ }
+
+ public int getIdentifier() {
+ return (this.identifier & 0xFF);
+ }
+
+ public int getLength() {
+ return this.data.length;
+ }
+
+ public byte[] getData() {
+ return this.data;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append(getClass().getSimpleName() + "[");
+ sb.append(PduUtils.byteToPdu(this.identifier));
+ sb.append(", ");
+ sb.append(PduUtils.byteToPdu(this.data.length));
+ sb.append(", ");
+ sb.append(PduUtils.bytesToPdu(this.data));
+ sb.append("]");
+ return sb.toString();
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/InformationElementFactory.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/InformationElementFactory.java
new file mode 100644
index 0000000000000..189d6f86ab0fb
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/InformationElementFactory.java
@@ -0,0 +1,53 @@
+package org.smslib.pduUtils.gsm3040.ie;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class InformationElementFactory {
+ // used to determine what InformationElement to use based on bytes from a UDH
+ // assumes the supplied bytes are correct
+ public static InformationElement createInformationElement(int id, byte[] data) {
+ byte iei = (byte) (id & 0xFF);
+ switch (iei) {
+ case ConcatInformationElement.CONCAT_8BIT_REF:
+ case ConcatInformationElement.CONCAT_16BIT_REF:
+ return new ConcatInformationElement(iei, data);
+ case PortInformationElement.PORT_16BIT:
+ return new PortInformationElement(iei, data);
+ default:
+ return new InformationElement(iei, data);
+ }
+ }
+
+ public static ConcatInformationElement generateConcatInfo(int mpRefNo, int partNo) {
+ ConcatInformationElement concatInfo = new ConcatInformationElement(
+ ConcatInformationElement.getDefaultConcatType(), mpRefNo, 1, partNo);
+ return concatInfo;
+ }
+
+ public static PortInformationElement generatePortInfo(int destPort, int srcPort) {
+ PortInformationElement portInfo = new PortInformationElement(PortInformationElement.PORT_16BIT, destPort,
+ srcPort);
+ return portInfo;
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/PortInformationElement.java b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/PortInformationElement.java
new file mode 100644
index 0000000000000..90f4bc790d27d
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/3rdparty/java/org/smslib/pduUtils/gsm3040/ie/PortInformationElement.java
@@ -0,0 +1,87 @@
+package org.smslib.pduUtils.gsm3040.ie;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+//PduUtils Library - A Java library for generating GSM 3040 Protocol Data Units (PDUs)
+//
+//Copyright (C) 2008, Ateneo Java Wireless Competency Center/Blueblade Technologies, Philippines.
+//PduUtils is distributed under the terms of the Apache License version 2.0
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+/**
+ * Extracted from SMSLib
+ */
+@NonNullByDefault
+public class PortInformationElement extends InformationElement {
+ public static final int PORT_16BIT = 0x05;
+
+ PortInformationElement(byte id, byte[] data) {
+ super(id, data);
+ if (getIdentifier() != PORT_16BIT) {
+ throw new IllegalArgumentException(
+ "Invalid identifier " + getIdentifier() + " in data in: " + getClass().getSimpleName());
+ }
+ // iei
+ // iel
+ // dest(2 bytes)
+ // src (2 bytes)
+ if (data.length != 4) {
+ throw new IllegalArgumentException("Invalid data length in: " + getClass().getSimpleName());
+ }
+ }
+
+ PortInformationElement(int identifier, int destPort, int srcPort) {
+ super((byte) (identifier & 0xFF), getData(identifier, destPort, srcPort));
+ }
+
+ private static byte[] getData(int identifier, int destPort, int srcPort) {
+ byte[] data = null;
+ switch (identifier) {
+ case PORT_16BIT:
+ data = new byte[4];
+ data[0] = (byte) ((destPort & 0xFF00) >>> 8);
+ data[1] = (byte) (destPort & 0xFF);
+ data[2] = (byte) ((srcPort & 0xFF00) >>> 8);
+ data[3] = (byte) (srcPort & 0xFF);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid identifier for PortInformationElement");
+ }
+ return data;
+ }
+
+ public int getDestPort() {
+ // first 2 bytes of data
+ byte[] data = getData();
+ return (((data[0] & 0xFF) << 8) | (data[1] & 0xFF));
+ }
+
+ public int getSrcPort() {
+ // next 2 bytes of data
+ byte[] data = getData();
+ return (((data[2] & 0xFF) << 8) | (data[3] & 0xFF));
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.append(super.toString());
+ sb.append("[Dst Port: ");
+ sb.append(getDestPort());
+ sb.append(", Src Port: ");
+ sb.append(getSrcPort());
+ sb.append("]");
+ return sb.toString();
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/feature/feature.xml b/bundles/org.openhab.binding.smsmodem/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..5a95e4f44905c
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab-transport-serial
+ mvn:org.openhab.addons.bundles/org.openhab.binding.smsmodem/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSConversationConfiguration.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSConversationConfiguration.java
new file mode 100644
index 0000000000000..5a05ed3dad23d
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSConversationConfiguration.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link SMSConversationConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@NonNullByDefault
+public class SMSConversationConfiguration {
+
+ public String recipient = "";
+ public boolean deliveryReport = false;
+ public String encoding = "Enc7";
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSConversationDiscoveryService.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSConversationDiscoveryService.java
new file mode 100644
index 0000000000000..c08f2ee3ed040
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSConversationDiscoveryService.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.smsmodem.internal.handler.SMSConversationHandler;
+import org.openhab.binding.smsmodem.internal.handler.SMSModemBridgeHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+
+/**
+ * This class implements a discovery service for SMSConversation
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@NonNullByDefault
+public class SMSConversationDiscoveryService extends AbstractDiscoveryService
+ implements DiscoveryService, ThingHandlerService {
+
+ private @NonNullByDefault({}) SMSModemBridgeHandler bridgeHandler;
+ private @NonNullByDefault({}) ThingUID bridgeUid;
+
+ public SMSConversationDiscoveryService() {
+ super(0);
+ }
+
+ public SMSConversationDiscoveryService(int timeout) throws IllegalArgumentException {
+ super(timeout);
+ }
+
+ @Override
+ protected void startScan() {
+ for (String msisdn : bridgeHandler.getAllSender()) {
+ buildDiscovery(msisdn);
+ }
+ }
+
+ public void buildDiscovery(String sender) {
+ String senderSanitized = sender.replaceAll("[^a-zA-Z0-9+]", "_");
+
+ ThingUID thingUID = new ThingUID(SMSModemBindingConstants.SMSCONVERSATION_THING_TYPE, senderSanitized,
+ bridgeUid.getId());
+
+ DiscoveryResult result = DiscoveryResultBuilder.create(thingUID)
+ .withProperty(SMSModemBindingConstants.SMSCONVERSATION_PARAMETER_RECIPIENT, senderSanitized)
+ .withLabel("Conversation with " + sender).withBridge(bridgeUid)
+ .withThingType(SMSModemBindingConstants.SMSCONVERSATION_THING_TYPE)
+ .withRepresentationProperty(SMSModemBindingConstants.SMSCONVERSATION_PARAMETER_RECIPIENT).build();
+ thingDiscovered(result);
+ }
+
+ public void buildByAutoDiscovery(String sender) {
+ if (isBackgroundDiscoveryEnabled()) {
+ buildDiscovery(sender);
+ }
+ }
+
+ @Override
+ public Set getSupportedThingTypes() {
+ return Set.of(SMSConversationHandler.SUPPORTED_THING_TYPES_UIDS);
+ }
+
+ @Override
+ public void setThingHandler(ThingHandler handler) {
+ this.bridgeHandler = (SMSModemBridgeHandler) handler;
+ this.bridgeUid = handler.getThing().getUID();
+ this.bridgeHandler.setDiscoveryService(this);
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return bridgeHandler;
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemBindingConstants.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemBindingConstants.java
new file mode 100644
index 0000000000000..d8f89b90f6f52
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemBindingConstants.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link SMSModemBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@NonNullByDefault
+public class SMSModemBindingConstants {
+
+ private static final String BINDING_ID = "smsmodem";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID SMSCONVERSATION_THING_TYPE = new ThingTypeUID(BINDING_ID, "smsconversation");
+ public static final ThingTypeUID SMSMODEMBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "smsmodembridge");
+ public static final ThingTypeUID SMSMODEMREMOTEBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID,
+ "smsmodemremotebridge");
+
+ // List of all Channel ids
+ public static final String CHANNEL_RECEIVED = "receive";
+ public static final String CHANNEL_SEND = "send";
+ public static final String CHANNEL_DELIVERYSTATUS = "deliverystatus";
+ public static final String CHANNEL_TRIGGER_MODEM_RECEIVE = "receivetrigger";
+
+ // parameter
+ public static final String SMSCONVERSATION_PARAMETER_RECIPIENT = "recipient";
+
+ // List of all properties
+ public static final String PROPERTY_MANUFACTURER = Thing.PROPERTY_VENDOR;
+ public static final String PROPERTY_MODEL = Thing.PROPERTY_MODEL_ID;
+ public static final String PROPERTY_SWVERSION = Thing.PROPERTY_FIRMWARE_VERSION;
+ public static final String PROPERTY_SERIALNO = Thing.PROPERTY_SERIAL_NUMBER;
+ public static final String PROPERTY_IMSI = "imsi";
+ public static final String PROPERTY_RSSI = "rssi";
+ public static final String PROPERTY_MODE = "mode";
+ public static final String PROPERTY_TOTALSENT = "sent";
+ public static final String PROPERTY_TOTALFAILED = "failed";
+ public static final String PROPERTY_TOTALRECEIVED = "received";
+ public static final String PROPERTY_TOTALFAILURE = "failure";
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemBridgeConfiguration.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemBridgeConfiguration.java
new file mode 100644
index 0000000000000..799d2f2d7e65d
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemBridgeConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link SMSModemBridgeConfiguration} class contains fields mapping bridge configuration parameters.
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@NonNullByDefault
+public class SMSModemBridgeConfiguration {
+
+ public String serialPort = "";
+ public Integer baud = 9600;
+ public String simPin = "";
+ public Integer pollingInterval = 15;
+ public Integer delayBetweenSend = 0;
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemHandlerFactory.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemHandlerFactory.java
new file mode 100644
index 0000000000000..66d6bd83997f1
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemHandlerFactory.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.smsmodem.internal.handler.SMSConversationHandler;
+import org.openhab.binding.smsmodem.internal.handler.SMSModemBridgeHandler;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link SMSModemHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@Component(configurationPid = "binding.smsmodem", service = ThingHandlerFactory.class)
+@NonNullByDefault
+public class SMSModemHandlerFactory extends BaseThingHandlerFactory {
+
+ public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet<>();
+ {
+ SUPPORTED_THING_TYPES_UIDS.add(SMSConversationHandler.SUPPORTED_THING_TYPES_UIDS);
+ SUPPORTED_THING_TYPES_UIDS.addAll(SMSModemBridgeHandler.SUPPORTED_THING_TYPES_UIDS);
+ }
+
+ private @NonNullByDefault({}) SerialPortManager serialPortManager;
+
+ @Reference
+ protected void setSerialPortManager(final SerialPortManager serialPortManager) {
+ this.serialPortManager = serialPortManager;
+ }
+
+ protected void unsetSerialPortManager(final SerialPortManager serialPortManager) {
+ this.serialPortManager = null;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (SMSModemBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
+ return new SMSModemBridgeHandler((Bridge) thing, serialPortManager);
+ } else if (SMSConversationHandler.SUPPORTED_THING_TYPES_UIDS.equals(thingTypeUID)) {
+ return new SMSConversationHandler(thing);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemRemoteBridgeConfiguration.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemRemoteBridgeConfiguration.java
new file mode 100644
index 0000000000000..5bae8fed41e92
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/SMSModemRemoteBridgeConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link SMSModemRemoteBridgeConfiguration} class contains fields mapping bridge configuration parameters.
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@NonNullByDefault
+public class SMSModemRemoteBridgeConfiguration {
+
+ public String ip = "";
+ public Integer networkPort = 2000;
+ public String simPin = "";
+ public Integer pollingInterval = 15;
+ public Integer delayBetweenSend = 0;
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/actions/SMSModemActions.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/actions/SMSModemActions.java
new file mode 100644
index 0000000000000..c0879534e6f91
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/actions/SMSModemActions.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal.actions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.smsmodem.internal.handler.SMSModemBridgeHandler;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.smslib.message.AbstractMessage.Encoding;
+
+/**
+ * The {@link SMSModemActions} exposes some actions
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@ThingActionsScope(name = "smsmodem")
+@NonNullByDefault
+public class SMSModemActions implements ThingActions {
+
+ private @NonNullByDefault({}) SMSModemBridgeHandler handler;
+
+ private final Logger logger = LoggerFactory.getLogger(SMSModemActions.class);
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ this.handler = (SMSModemBridgeHandler) handler;
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return handler;
+ }
+
+ @RuleAction(label = "Send Message With Special Encoding", description = "Send a message and specify encoding")
+ public void sendSMS(
+ @ActionInput(name = "recipient", label = "recipient", description = "Recipient of the message") @Nullable String recipient,
+ @ActionInput(name = "message", label = "message", description = "Message to send") @Nullable String message,
+ @ActionInput(name = "encoding", label = "encoding", description = "Encoding") @Nullable String encoding) {
+ if (recipient != null && !recipient.isEmpty() && message != null) {
+ handler.send(recipient, message, false, encoding);
+ } else {
+ logger.warn("SMSModem cannot send a message with no recipient or text");
+ }
+ }
+
+ @RuleAction(label = "Send Message", description = "Send a message")
+ public void sendSMS(
+ @ActionInput(name = "recipient", label = "recipient", description = "Recipient of the message") @Nullable String recipient,
+ @ActionInput(name = "message", label = "message", description = "Message to send") @Nullable String message) {
+ sendSMS(recipient, message, Encoding.Enc7.toString());
+ }
+
+ public static void sendSMS(@Nullable ThingActions actions, @Nullable String recipient, @Nullable String message,
+ @Nullable String encoding) {
+ if (actions instanceof SMSModemActions) {
+ ((SMSModemActions) actions).sendSMS(recipient, message, encoding);
+ } else {
+ throw new IllegalArgumentException("Instance is not an SMSModemActions class.");
+ }
+ }
+
+ public static void sendSMS(@Nullable ThingActions actions, @Nullable String recipient, @Nullable String message) {
+ sendSMS(actions, recipient, message, Encoding.Enc7.toString());
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/DeliveryStatus.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/DeliveryStatus.java
new file mode 100644
index 0000000000000..bf351e4128cf9
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/DeliveryStatus.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * DeliveryStatus enum for delivery report status
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@NonNullByDefault
+public enum DeliveryStatus {
+ UNKNOWN,
+ QUEUED,
+ SENT,
+ PENDING,
+ DELIVERED,
+ EXPIRED,
+ FAILED
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/ModemConfigurationException.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/ModemConfigurationException.java
new file mode 100644
index 0000000000000..668538f42100e
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/ModemConfigurationException.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * Exception class for SMSLib configuration
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@NonNullByDefault
+public class ModemConfigurationException extends Exception {
+
+ private static final long serialVersionUID = -3455806333751297448L;
+
+ public ModemConfigurationException(String message) {
+ super(message);
+ }
+
+ public ModemConfigurationException(String message, Exception cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/SMSConversationHandler.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/SMSConversationHandler.java
new file mode 100644
index 0000000000000..614cab8a954a5
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/SMSConversationHandler.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.smsmodem.internal.SMSConversationConfiguration;
+import org.openhab.binding.smsmodem.internal.SMSModemBindingConstants;
+import org.openhab.core.i18n.ConfigurationException;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SMSConversationHandler} is responsible for managing
+ * discussion channels.
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@NonNullByDefault
+public class SMSConversationHandler extends BaseThingHandler {
+
+ public static final ThingTypeUID SUPPORTED_THING_TYPES_UIDS = SMSModemBindingConstants.SMSCONVERSATION_THING_TYPE;
+
+ private final Logger logger = LoggerFactory.getLogger(SMSConversationHandler.class);
+
+ private @Nullable SMSModemBridgeHandler bridgeHandler;
+
+ private SMSConversationConfiguration config;
+
+ public SMSConversationHandler(Thing thing) {
+ super(thing);
+ this.config = new SMSConversationConfiguration();
+ }
+
+ public String getRecipient() {
+ return config.recipient.trim();
+ }
+
+ private synchronized void checkBridgeHandler() {
+ if (this.bridgeHandler == null) {
+ Bridge bridge = getBridge();
+ if (bridge == null) {
+ throw new ConfigurationException("Required bridge not defined for SMSconversation {} with {}.",
+ thing.getUID(), getRecipient());
+ }
+ ThingHandler handler = bridge.getHandler();
+ if (handler instanceof SMSModemBridgeHandler) {
+ this.bridgeHandler = (SMSModemBridgeHandler) handler;
+ } else {
+ throw new ConfigurationException("No available bridge handler found for SMSConversation {} bridge {} .",
+ thing.getUID(), bridge.getUID());
+ }
+ }
+ }
+
+ protected void checkAndReceive(String sender, String text) {
+ String conversationRecipient = config.recipient.trim();
+ // is the recipient the one handled by this conversation ? :
+ if (conversationRecipient.equals(sender)) {
+ updateState(SMSModemBindingConstants.CHANNEL_RECEIVED, new StringType(text));
+ }
+ }
+
+ protected void checkAndUpdateDeliveryStatus(String messageRecipient, DeliveryStatus sentStatus) {
+ String conversationRecipient = config.recipient.trim();
+ // is the recipient the one handled by this conversation ? :
+ if (conversationRecipient.equals(messageRecipient)) {
+ updateState(SMSModemBindingConstants.CHANNEL_DELIVERYSTATUS, new StringType(sentStatus.name()));
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ return;
+ }
+ if (channelUID.getId().equals(SMSModemBindingConstants.CHANNEL_SEND)) {
+ send(command.toString());
+ updateState(SMSModemBindingConstants.CHANNEL_SEND, new StringType(command.toString()));
+ }
+ }
+
+ public void send(String text) {
+ SMSModemBridgeHandler bridgeHandlerFinal = bridgeHandler;
+ if (bridgeHandlerFinal != null) {
+ bridgeHandlerFinal.send(getRecipient(), text, config.deliveryReport, config.encoding);
+ } else {
+ logger.warn("Only channel 'send' in SMSConversation can receive command");
+ }
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(SMSConversationConfiguration.class);
+ try {
+ checkBridgeHandler();
+ updateStatus(ThingStatus.ONLINE);
+ } catch (ConfigurationException confe) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, confe.getMessage());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/SMSModemBridgeHandler.java b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/SMSModemBridgeHandler.java
new file mode 100644
index 0000000000000..06e80a8329767
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/java/org/openhab/binding/smsmodem/internal/handler/SMSModemBridgeHandler.java
@@ -0,0 +1,492 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.smsmodem.internal.handler;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.smsmodem.internal.SMSConversationDiscoveryService;
+import org.openhab.binding.smsmodem.internal.SMSModemBindingConstants;
+import org.openhab.binding.smsmodem.internal.SMSModemBridgeConfiguration;
+import org.openhab.binding.smsmodem.internal.SMSModemRemoteBridgeConfiguration;
+import org.openhab.binding.smsmodem.internal.actions.SMSModemActions;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.io.transport.serial.SerialPortIdentifier;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.smslib.CommunicationException;
+import org.smslib.Modem;
+import org.smslib.Modem.Status;
+import org.smslib.callback.IDeviceInformationListener;
+import org.smslib.callback.IInboundOutboundMessageListener;
+import org.smslib.callback.IModemStatusListener;
+import org.smslib.message.AbstractMessage.Encoding;
+import org.smslib.message.DeliveryReportMessage;
+import org.smslib.message.InboundMessage;
+import org.smslib.message.MsIsdn;
+import org.smslib.message.OutboundMessage;
+import org.smslib.message.Payload;
+import org.smslib.message.Payload.Type;
+
+/**
+ * The {@link SMSModemBridgeHandler} is responsible for handling
+ * communication with the modem.
+ *
+ * @author Gwendal ROULLEAU - Initial contribution
+ */
+@NonNullByDefault
+public class SMSModemBridgeHandler extends BaseBridgeHandler
+ implements IModemStatusListener, IInboundOutboundMessageListener, IDeviceInformationListener {
+
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(
+ SMSModemBindingConstants.SMSMODEMBRIDGE_THING_TYPE,
+ SMSModemBindingConstants.SMSMODEMREMOTEBRIDGE_THING_TYPE);
+
+ private final Logger logger = LoggerFactory.getLogger(SMSModemBridgeHandler.class);
+
+ private SerialPortManager serialPortManager;
+
+ /**
+ * The smslib object responsible for the serial communication with the modem
+ */
+ private @Nullable Modem modem;
+
+ /**
+ * A scheduled watchdog check
+ */
+ private @Nullable ScheduledFuture> checkScheduled;
+
+ // we keep a list of msisdn sender for autodiscovery
+ private Set senderMsisdn = new HashSet();
+ private @Nullable SMSConversationDiscoveryService discoveryService;
+
+ private boolean shouldRun = false;
+
+ public SMSModemBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) {
+ super(bridge);
+ this.serialPortManager = serialPortManager;
+ }
+
+ @Override
+ public void dispose() {
+ shouldRun = false;
+ ScheduledFuture> checkScheduledFinal = checkScheduled;
+ if (checkScheduledFinal != null) {
+ checkScheduledFinal.cancel(true);
+ }
+ Modem finalModem = modem;
+ if (finalModem != null) {
+ scheduler.execute(finalModem::stop);
+ finalModem.registerStatusListener(null);
+ finalModem.registerMessageListener(null);
+ finalModem.registerInformationListener(null);
+ }
+ modem = null;
+ }
+
+ @Override
+ protected void updateConfiguration(Configuration configuration) {
+ super.updateConfiguration(configuration);
+ scheduler.execute(() -> {
+ Modem finalModem = modem;
+ if (finalModem != null) {
+ finalModem.stop();
+ }
+ checkAndStartModemIfNeeded();
+ });
+ }
+
+ @Override
+ public void initialize() {
+ updateStatus(ThingStatus.UNKNOWN);
+ shouldRun = true;
+ ScheduledFuture> checkScheduledFinal = checkScheduled;
+ if (checkScheduledFinal == null || (checkScheduledFinal.isDone()) && this.shouldRun) {
+ checkScheduled = scheduler.scheduleWithFixedDelay(this::checkAndStartModemIfNeeded, 0, 15,
+ TimeUnit.SECONDS);
+ }
+ }
+
+ private synchronized void checkAndStartModemIfNeeded() {
+ try {
+ if (shouldRun && !isRunning()) {
+ logger.debug("Initializing smsmodem");
+ // ensure the underlying modem is stopped before trying to (re)starting it :
+ Modem finalModem = modem;
+ if (finalModem != null) {
+ finalModem.stop();
+ }
+ String logName;
+ if (getThing().getThingTypeUID().equals(SMSModemBindingConstants.SMSMODEMBRIDGE_THING_TYPE)) {
+ SMSModemBridgeConfiguration config = getConfigAs(SMSModemBridgeConfiguration.class);
+ modem = new Modem(serialPortManager, resolveEventualSymbolicLink(config.serialPort),
+ Integer.valueOf(config.baud), config.simPin, scheduler, config.pollingInterval,
+ config.delayBetweenSend);
+ checkParam(config);
+ logName = config.serialPort + " | " + config.baud;
+ } else if (getThing().getThingTypeUID()
+ .equals(SMSModemBindingConstants.SMSMODEMREMOTEBRIDGE_THING_TYPE)) {
+ SMSModemRemoteBridgeConfiguration config = getConfigAs(SMSModemRemoteBridgeConfiguration.class);
+ modem = new Modem(serialPortManager, resolveEventualSymbolicLink(config.ip),
+ Integer.valueOf(config.networkPort), config.simPin, scheduler, config.pollingInterval,
+ config.delayBetweenSend);
+ checkRemoteParam(config);
+ logName = config.ip + ":" + config.networkPort;
+ } else {
+ throw new IllegalArgumentException("Invalid thing type");
+ }
+ logger.debug("Now trying to start SMSModem {}", logName);
+ finalModem = modem;
+ if (finalModem != null) {
+ finalModem.registerStatusListener(this);
+ finalModem.registerMessageListener(this);
+ finalModem.registerInformationListener(this);
+ finalModem.start();
+ }
+ logger.debug("SMSModem {} started", logName);
+ }
+ } catch (ModemConfigurationException e) {
+ String message = e.getMessage();
+ if (e.getCause() != null && e.getCause() instanceof IOException) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message);
+ }
+ }
+ }
+
+ private void checkParam(SMSModemBridgeConfiguration config) throws ModemConfigurationException {
+ String realSerialPort = resolveEventualSymbolicLink(config.serialPort);
+ SerialPortIdentifier identifier = serialPortManager.getIdentifier(realSerialPort);
+ if (identifier == null) {
+ // no serial port
+ throw new ModemConfigurationException(
+ realSerialPort + " with " + config.baud + " is not a valid serial port | baud");
+ }
+ }
+
+ private void checkRemoteParam(SMSModemRemoteBridgeConfiguration config) throws ModemConfigurationException {
+ try {
+ InetAddress inetAddress = InetAddress.getByName(config.ip);
+ String ip = inetAddress.getHostAddress();
+
+ // test reachable address :
+ try (Socket s = new Socket(ip, config.networkPort)) {
+ }
+ } catch (IOException | NumberFormatException ex) {
+ // no ip
+ throw new ModemConfigurationException(
+ config.ip + ":" + config.networkPort + " is not a reachable address:port", ex);
+ }
+ }
+
+ private String resolveEventualSymbolicLink(String serialPortOrIp) {
+ String keepResult = serialPortOrIp;
+ Path maybePath = Paths.get(serialPortOrIp);
+ File maybeFile = maybePath.toFile();
+ if (maybeFile.exists() && Files.isSymbolicLink(maybePath)) {
+ try {
+ maybePath = maybePath.toRealPath();
+ keepResult = maybePath.toAbsolutePath().toString();
+ } catch (IOException e) {
+ } // nothing to do, not a valid symbolic link, return
+ }
+ return keepResult;
+ }
+
+ public boolean isRunning() {
+ Modem finalModem = modem;
+ return finalModem != null
+ && (finalModem.getStatus() == Status.Started || finalModem.getStatus() == Status.Starting);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ }
+
+ @Override
+ public void messageReceived(InboundMessage message) {
+ String sender = message.getOriginatorAddress().getAddress();
+ Payload payload = message.getPayload();
+ String messageText;
+ if (payload.getType().equals(Type.Text)) {
+ String text = payload.getText();
+ if (text != null) {
+ messageText = text;
+ } else {
+ logger.warn("Message has no payload !");
+ return;
+ }
+ } else {
+ byte[] bytes = payload.getBytes();
+ if (bytes != null) {
+ logger.warn("Message payload in binary format. Don't know how to handle it. Please report it.");
+ messageText = bytes.toString();
+ } else {
+ logger.warn("Message has no payload !");
+ return;
+ }
+ }
+ logger.debug("Receiving new message from {} : {}", sender, messageText);
+
+ // dispatch to conversation :
+ for (SMSConversationHandler child : getChildHandlers()) {
+ child.checkAndReceive(sender, messageText);
+ }
+
+ // channel trigger
+ String recipientAndMessage = sender + "|" + messageText;
+ triggerChannel(SMSModemBindingConstants.CHANNEL_TRIGGER_MODEM_RECEIVE, recipientAndMessage);
+
+ // prepare discovery service
+ senderMsisdn.add(sender);
+ final SMSConversationDiscoveryService finalDiscoveryService = discoveryService;
+ if (finalDiscoveryService != null) {
+ finalDiscoveryService.buildByAutoDiscovery(sender);
+ }
+ try { // delete message on the sim
+ Modem finalModem = modem;
+ if (finalModem != null) {
+ finalModem.delete(message);
+ }
+ } catch (CommunicationException e) {
+ logger.warn("Cannot delete message after receiving it !", e);
+ }
+ }
+
+ /**
+ * Send message
+ *
+ * @param recipient The recipient for the message
+ * @param text The message content
+ * @param deliveryReport If we should ask the network for a delivery report
+ */
+ public void send(String recipient, String text, boolean deliveryReport, @Nullable String encoding) {
+ OutboundMessage out = new OutboundMessage(recipient, text);
+ try {
+ if (encoding != null && !encoding.isEmpty()) {
+ Encoding encoding2 = Encoding.valueOf(encoding);
+ out.setEncoding(encoding2);
+ }
+ } catch (IllegalArgumentException e) {
+ logger.warn("Encoding {} is not supported. Use Enc7, Enc8, EncUcs2, or EncCustom", encoding);
+ }
+ out.setRequestDeliveryReport(deliveryReport);
+ logger.debug("Sending message to {}", recipient);
+ Modem finalModem = modem;
+ if (finalModem != null) {
+ finalModem.queue(out);
+ }
+ }
+
+ /**
+ * Used by the scanning discovery service to create conversation
+ *
+ * @return All senders of the received messages since the last start
+ */
+ public Set getAllSender() {
+ return new HashSet<>(senderMsisdn);
+ }
+
+ @Override
+ public Collection> getServices() {
+ return Set.of(SMSModemActions.class, SMSConversationDiscoveryService.class);
+ }
+
+ @Override
+ public boolean processStatusCallback(Modem.Status oldStatus, Modem.Status newStatus) {
+ switch (newStatus) {
+ case Error:
+ String finalDescription = "unknown";
+ Modem finalModem = modem;
+ if (finalModem != null) {
+ finalDescription = finalModem.getDescription();
+ }
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "SMSLib reported an error on the underlying modem " + finalDescription);
+ break;
+ case Started:
+ updateStatus(ThingStatus.ONLINE);
+ break;
+ case Starting:
+ updateStatus(ThingStatus.UNKNOWN);
+ break;
+ case Stopped:
+ if (thing.getStatus() != ThingStatus.OFFLINE) {
+ updateStatus(ThingStatus.OFFLINE);
+ }
+ break;
+ case Stopping:
+ if (thing.getStatus() != ThingStatus.OFFLINE) {
+ updateStatus(ThingStatus.OFFLINE);
+ }
+ break;
+ }
+ return false;
+ }
+
+ public void setDiscoveryService(SMSConversationDiscoveryService smsConversationDiscoveryService) {
+ this.discoveryService = smsConversationDiscoveryService;
+ }
+
+ @Override
+ public void messageSent(OutboundMessage message) {
+ DeliveryStatus sentStatus;
+ switch (message.getSentStatus()) {
+ case Failed:
+ sentStatus = DeliveryStatus.FAILED;
+ break;
+ case Unsent:
+ case Queued:
+ sentStatus = DeliveryStatus.QUEUED;
+ break;
+ case Sent:
+ sentStatus = DeliveryStatus.SENT;
+ break;
+ default: // shoult not happened
+ sentStatus = DeliveryStatus.UNKNOWN;
+ break;
+ }
+ // dispatch to conversation :
+ MsIsdn recipientAddress = message.getRecipientAddress();
+ if (recipientAddress != null) {
+ String recipient = recipientAddress.getAddress();
+ for (SMSConversationHandler child : getChildHandlers()) {
+ child.checkAndUpdateDeliveryStatus(recipient, sentStatus);
+ }
+ }
+ }
+
+ @Override
+ public void messageDelivered(DeliveryReportMessage message) {
+ DeliveryStatus sentStatus;
+ switch (message.getDeliveryStatus()) {
+ case Delivered:
+ sentStatus = DeliveryStatus.DELIVERED;
+ break;
+ case Error:
+ case Failed:
+ sentStatus = DeliveryStatus.FAILED;
+ break;
+ case Expired:
+ sentStatus = DeliveryStatus.EXPIRED;
+ break;
+ case Pending:
+ sentStatus = DeliveryStatus.PENDING;
+ break;
+ case Unknown:
+ default:
+ sentStatus = DeliveryStatus.UNKNOWN;
+ break;
+ }
+ MsIsdn recipientAddress = message.getRecipientAddress();
+ if (recipientAddress != null) {
+ String recipient = recipientAddress.getAddress();
+ for (SMSConversationHandler child : getChildHandlers()) {
+ child.checkAndUpdateDeliveryStatus(recipient, sentStatus);
+ }
+ }
+ try {
+ Modem finalModem = modem;
+ if (finalModem != null) {
+ finalModem.delete(message);
+ }
+ } catch (CommunicationException e) {
+ logger.warn("Cannot delete delivery report after receiving it !", e);
+ }
+ }
+
+ private Set getChildHandlers() {
+ return getThing().getThings().stream().map(Thing::getHandler).filter(Objects::nonNull)
+ .map(handler -> (SMSConversationHandler) handler).collect(Collectors.toSet());
+ }
+
+ @Override
+ public void setManufacturer(String manufacturer) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_MANUFACTURER, manufacturer);
+ }
+
+ @Override
+ public void setModel(String model) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_MODEL, model);
+ }
+
+ @Override
+ public void setSwVersion(String swVersion) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_SWVERSION, swVersion);
+ }
+
+ @Override
+ public void setSerialNo(String serialNo) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_SERIALNO, serialNo);
+ }
+
+ @Override
+ public void setImsi(String imsi) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_IMSI, imsi);
+ }
+
+ @Override
+ public void setRssi(String rssi) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_RSSI, rssi);
+ }
+
+ @Override
+ public void setMode(String mode) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_MODE, mode);
+ }
+
+ @Override
+ public void setTotalSent(String totalSent) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALSENT, totalSent);
+ }
+
+ @Override
+ public void setTotalFailed(String totalFailed) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALFAILED, totalFailed);
+ }
+
+ @Override
+ public void setTotalReceived(String totalReceived) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALRECEIVED, totalReceived);
+ }
+
+ @Override
+ public void setTotalFailures(String totalFailure) {
+ thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALFAILURE, totalFailure);
+ }
+}
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..619e6fd783636
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ SMSModem Binding
+ This binding handles a GSM modem connected to the openHAB server (Serial), or exposed on the network. It
+ can send and receive SMS.
+
+
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/i18n/smsmodem.properties b/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/i18n/smsmodem.properties
new file mode 100644
index 0000000000000..3634b7d0f05ed
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/i18n/smsmodem.properties
@@ -0,0 +1,49 @@
+# binding
+
+binding.smsmodem.name = SMSModem Binding
+binding.smsmodem.description = This binding handle a GSM modem connected to the openHAB server (Serial), or exposed on the network. It can send and receive SMS.
+
+# thing types
+
+thing-type.smsmodem.smsconversation.label = SMS Conversation
+thing-type.smsmodem.smsconversation.description = Represents a conversation with a SMS recipient.
+thing-type.smsmodem.smsmodembridge.label = SMSModem Bridge
+thing-type.smsmodem.smsmodembridge.description = This bridge represents a serial modem.
+thing-type.smsmodem.smsmodemremotebridge.label = SMSModem Remote Bridge
+thing-type.smsmodem.smsmodemremotebridge.description = This bridge represents a modem exposed over the network.
+
+# thing types config
+
+thing-type.config.smsmodem.smsconversation.deliveryReport.label = Delivery Report
+thing-type.config.smsmodem.smsconversation.deliveryReport.description = Ask network for delivery report.
+thing-type.config.smsmodem.smsconversation.encoding.label = Encoding
+thing-type.config.smsmodem.smsconversation.encoding.description = Encoding for the message to send. Default Enc7.
+thing-type.config.smsmodem.smsconversation.recipient.label = Recipient Number
+thing-type.config.smsmodem.smsconversation.recipient.description = The SMS number of the recipient.
+thing-type.config.smsmodem.smsmodembridge.baud.label = Baud
+thing-type.config.smsmodem.smsmodembridge.baud.description = Baud rate.
+thing-type.config.smsmodem.smsmodembridge.delayBetweenSend.description = Delay between two messages (in milliseconds). Useful for slow modem.
+thing-type.config.smsmodem.smsmodembridge.pollingInterval.description = Delay between polling for new messages (in seconds).
+thing-type.config.smsmodem.smsmodembridge.serialPort.label = Serial Port
+thing-type.config.smsmodem.smsmodembridge.serialPort.description = Serial port of the modem (usually /dev/ttyUSB0).
+thing-type.config.smsmodem.smsmodembridge.simPin.label = Pin Code
+thing-type.config.smsmodem.smsmodembridge.simPin.description = The pin (if set) for the sim card.
+thing-type.config.smsmodem.smsmodemremotebridge.delayBetweenSend.description = Delay between two messages (in milliseconds). Useful for slow modem.
+thing-type.config.smsmodem.smsmodemremotebridge.ip.label = Address
+thing-type.config.smsmodem.smsmodemremotebridge.ip.description = IP address of the remote computer.
+thing-type.config.smsmodem.smsmodemremotebridge.networkPort.label = Network Port
+thing-type.config.smsmodem.smsmodemremotebridge.networkPort.description = Network port to join the remote service (a.k.a. ser2net).
+thing-type.config.smsmodem.smsmodemremotebridge.pollingInterval.description = Delay between polling for new messages (in seconds).
+thing-type.config.smsmodem.smsmodemremotebridge.simPin.label = Pin Code
+thing-type.config.smsmodem.smsmodemremotebridge.simPin.description = The pin (if set) for the sim card.
+
+# channel types
+
+channel-type.smsmodem.deliverystatus.label = Delivery Status
+channel-type.smsmodem.deliverystatus.description = Last message delivery status (either UNKNOWN, QUEUED, SENT, PENDING, DELIVERED, EXPIRED, or FAILED)
+channel-type.smsmodem.receive.label = Message Received
+channel-type.smsmodem.receive.description = Last message received
+channel-type.smsmodem.send.label = Send Message
+channel-type.smsmodem.send.description = Message to send to the recipient.
+channel-type.smsmodem.smsmodemreceivetrigger.label = Message Received
+channel-type.smsmodem.smsmodemreceivetrigger.description = Triggered when a message is received, in the form "|"
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/i18n/smsmodem_fr.properties b/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/i18n/smsmodem_fr.properties
new file mode 100644
index 0000000000000..b6e5e40fd00c8
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/i18n/smsmodem_fr.properties
@@ -0,0 +1,49 @@
+# binding
+
+binding.smsmodem.name = Extension SMSModem
+binding.smsmodem.description = Cette extension gère un modem GSM supportant les messages AT et connecté en série, ou exposé sur le réseau. Elle peut envoyer et recevoir des SMS
+
+# thing types
+
+thing-type.smsmodem.smsconversation.label = Conversation SMS
+thing-type.smsmodem.smsconversation.description = Représente une conversation avec un correspondant.
+thing-type.smsmodem.smsmodembridge.label = SMS Modem
+thing-type.smsmodem.smsmodembridge.description = Un modem connecté en série.
+thing-type.smsmodem.smsmodemremotebridge.label = SMS Remote Modem
+thing-type.smsmodem.smsmodemremotebridge.description = Un modem connecté par le réseau.
+
+# thing types config
+
+thing-type.config.smsmodem.smsconversation.deliveryReport.label = Accusé de réception
+thing-type.config.smsmodem.smsconversation.deliveryReport.description = Demande au réseau un accusé de réception.
+thing-type.config.smsmodem.smsconversation.encoding.label = Encodage
+thing-type.config.smsmodem.smsconversation.encoding.description = Encodage du message à envoyer. Défaut Enc7.
+thing-type.config.smsmodem.smsconversation.recipient.label = Numéro Du Correspondant
+thing-type.config.smsmodem.smsconversation.recipient.description = Le numéro SMS du correspondant.
+thing-type.config.smsmodem.smsmodembridge.baud.label = Taux (Baud)
+thing-type.config.smsmodem.smsmodembridge.baud.description = Taux de transmission.
+thing-type.config.smsmodem.smsmodembridge.delayBetweenSend.description = Délai entre deux envois (en millisecondes). Peut être utile pour les modems lents.
+thing-type.config.smsmodem.smsmodembridge.pollingInterval.description = Délai entre deux essais de récupération de message (in seconds).
+thing-type.config.smsmodem.smsmodembridge.serialPort.label = Port Série
+thing-type.config.smsmodem.smsmodembridge.serialPort.description = Port série du modem (habituellement /dev/ttyUSB0).
+thing-type.config.smsmodem.smsmodembridge.simPin.label = Code PIN
+thing-type.config.smsmodem.smsmodembridge.simPin.description = Le code PIN (si nécessaire) de la carte SIM.
+thing-type.config.smsmodem.smsmodemremotebridge.delayBetweenSend.description = Délai entre deux envois (en millisecondes). Peut être utile pour les modems lents.
+thing-type.config.smsmodem.smsmodemremotebridge.ip.label = Addresse
+thing-type.config.smsmodem.smsmodemremotebridge.ip.description = Addresse IP.
+thing-type.config.smsmodem.smsmodemremotebridge.networkPort.label = Port Réseau
+thing-type.config.smsmodem.smsmodemremotebridge.networkPort.description = Port réseau pour joindre le serveur (i.e. ser2net)
+thing-type.config.smsmodem.smsmodemremotebridge.pollingInterval.description = Délai entre deux essais de récupération de message (in seconds).
+thing-type.config.smsmodem.smsmodemremotebridge.simPin.label = Code PIN
+thing-type.config.smsmodem.smsmodemremotebridge.simPin.description = Le code PIN (si nécessaire) de la carte SIM.
+
+# channel types
+
+channel-type.smsmodem.deliverystatus.label = Accusé De Réception
+channel-type.smsmodem.deliverystatus.description = Dernier statut de message (soit UNKNOWN, QUEUED, SENT, PENDING, DELIVERED, EXPIRED, ou FAILED)
+channel-type.smsmodem.receive.label = Message Reçu
+channel-type.smsmodem.receive.description = Dernier message reçu
+channel-type.smsmodem.send.label = Message Envoyé
+channel-type.smsmodem.send.description = Message à envoyer au correspondant
+channel-type.smsmodem.smsmodemreceivetrigger.label = Message Reçu
+channel-type.smsmodem.smsmodemreceivetrigger.description = Déclenché quand un message est réceptionné, sous la forme "|"
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/thing/smsconversation.xml b/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/thing/smsconversation.xml
new file mode 100644
index 0000000000000..e15c64e9d2315
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/thing/smsconversation.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+ Represents a conversation with a SMS recipient.
+
+
+
+
+
+
+
+ recipient
+
+
+
+
+ The SMS number of the recipient.
+
+
+
+ Ask network for delivery report.
+ false
+
+
+
+
+
+
+
+
+
+ Encoding for the message to send. Default Enc7
+ Enc7
+
+
+
+
+
+ String
+
+ Message to send to the recipient.
+
+
+
+ String
+
+ Last message received
+
+
+
+ String
+
+ Last message delivery status (either UNKNOWN, QUEUED, SENT, PENDING, DELIVERED, EXPIRED, or FAILED)
+
+
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/thing/smsmodem.xml b/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/thing/smsmodem.xml
new file mode 100644
index 0000000000000..da35c44eb719e
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/resources/OH-INF/thing/smsmodem.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+ This bridge represents a modem.
+
+
+
+
+
+
+
+
+ Serial port of the modem (usually /dev/ttyUSB0).
+ serial-port
+
+
+
+
+ Baud rate.
+ 19200
+
+
+
+ The pin (if set) for the sim card.
+
+
+
+ true
+ 15
+ Delay between polling for new messages (in seconds).
+
+
+ true
+ 100
+ Delay between two messages (in milliseconds). Useful for slow modem.
+
+
+
+
+
+
+ This bridge represents a modem on a network controlled computer.
+
+
+
+
+
+
+
+
+ IP address of the remote computer.
+
+ network-address
+
+
+
+ Network port to join the remote service (a.k.a. ser2net).
+ 2000
+
+
+
+ The pin (if set) for the sim card.
+
+
+
+ true
+ 15
+ Delay between polling for new messages (in seconds).
+
+
+ true
+ 100
+ Delay between two messages (in milliseconds). Useful for slow modem.
+
+
+
+
+
+ trigger
+
+ Triggered when a message is received, in the form "<msisdn_sender>|<text>"
+
+
+
+
diff --git a/bundles/org.openhab.binding.smsmodem/src/main/resources/modem.properties b/bundles/org.openhab.binding.smsmodem/src/main/resources/modem.properties
new file mode 100644
index 0000000000000..f72b632aec6ed
--- /dev/null
+++ b/bundles/org.openhab.binding.smsmodem/src/main/resources/modem.properties
@@ -0,0 +1,22 @@
+default.poll_reader=100
+default.command_wait_unit=700
+default.after_ip_connect_wait_unit=5000
+default.wait_unit=200
+default.char_wait_unit=10
+default.timeout=30000
+default.port_buffer=8192
+default.delay_after_init1=10
+default.delay_after_init2=10
+default.delay_on_sim_error=5
+default.delay_network_registration=10
+default.delay_before_send_pdu=1
+default.delay_after_pre_pin=1
+default.delay_after_post_pin=1
+default.cpin_without_ok=0
+default.flowcontrol=IN
+huawei.init1=AT+CFUN=1\r
+huawei.init2=AT^CURC=0\r
+huawei.memory_locations=SMSR
+huawei_e3131.memory_locations=SM
+wavecommodem.memory_locations=SMSR
+wavecommodem.cpin_without_ok=1
diff --git a/bundles/pom.xml b/bundles/pom.xml
index efe7f1bc2eb0c..e94088968edc6 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -336,8 +336,9 @@
org.openhab.binding.sleepiq
org.openhab.binding.smaenergymeter
org.openhab.binding.smartmeter
- org.openhab.binding.smhi
org.openhab.binding.smartthings
+ org.openhab.binding.smhi
+ org.openhab.binding.smsmodem
org.openhab.binding.sncf
org.openhab.binding.snmp
org.openhab.binding.solaredge