2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.smsmodem.internal.handler;
16 import java.io.IOException;
17 import java.net.InetAddress;
18 import java.net.Socket;
19 import java.nio.file.Files;
20 import java.nio.file.Path;
21 import java.nio.file.Paths;
22 import java.util.Collection;
23 import java.util.HashSet;
24 import java.util.Objects;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.stream.Collectors;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.smsmodem.internal.SMSConversationDiscoveryService;
33 import org.openhab.binding.smsmodem.internal.SMSModemBindingConstants;
34 import org.openhab.binding.smsmodem.internal.SMSModemBridgeConfiguration;
35 import org.openhab.binding.smsmodem.internal.SMSModemRemoteBridgeConfiguration;
36 import org.openhab.binding.smsmodem.internal.actions.SMSModemActions;
37 import org.openhab.core.config.core.Configuration;
38 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
39 import org.openhab.core.io.transport.serial.SerialPortManager;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.ThingTypeUID;
46 import org.openhab.core.thing.binding.BaseBridgeHandler;
47 import org.openhab.core.thing.binding.ThingHandlerService;
48 import org.openhab.core.types.Command;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51 import org.smslib.CommunicationException;
52 import org.smslib.Modem;
53 import org.smslib.Modem.Status;
54 import org.smslib.callback.IDeviceInformationListener;
55 import org.smslib.callback.IInboundOutboundMessageListener;
56 import org.smslib.callback.IModemStatusListener;
57 import org.smslib.message.AbstractMessage.Encoding;
58 import org.smslib.message.DeliveryReportMessage;
59 import org.smslib.message.InboundMessage;
60 import org.smslib.message.MsIsdn;
61 import org.smslib.message.OutboundMessage;
62 import org.smslib.message.Payload;
63 import org.smslib.message.Payload.Type;
66 * The {@link SMSModemBridgeHandler} is responsible for handling
67 * communication with the modem.
69 * @author Gwendal ROULLEAU - Initial contribution
72 public class SMSModemBridgeHandler extends BaseBridgeHandler
73 implements IModemStatusListener, IInboundOutboundMessageListener, IDeviceInformationListener {
75 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(
76 SMSModemBindingConstants.SMSMODEMBRIDGE_THING_TYPE,
77 SMSModemBindingConstants.SMSMODEMREMOTEBRIDGE_THING_TYPE);
79 private final Logger logger = LoggerFactory.getLogger(SMSModemBridgeHandler.class);
81 private SerialPortManager serialPortManager;
84 * The smslib object responsible for the serial communication with the modem
86 private @Nullable Modem modem;
89 * A scheduled watchdog check
91 private @Nullable ScheduledFuture<?> checkScheduled;
93 // we keep a list of msisdn sender for autodiscovery
94 private Set<String> senderMsisdn = new HashSet<String>();
95 private @Nullable SMSConversationDiscoveryService discoveryService;
97 private boolean shouldRun = false;
99 public SMSModemBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) {
101 this.serialPortManager = serialPortManager;
105 public void dispose() {
107 ScheduledFuture<?> checkScheduledFinal = checkScheduled;
108 if (checkScheduledFinal != null) {
109 checkScheduledFinal.cancel(true);
111 Modem finalModem = modem;
112 if (finalModem != null) {
113 scheduler.execute(finalModem::stop);
114 finalModem.registerStatusListener(null);
115 finalModem.registerMessageListener(null);
116 finalModem.registerInformationListener(null);
122 protected void updateConfiguration(Configuration configuration) {
123 super.updateConfiguration(configuration);
124 scheduler.execute(() -> {
125 Modem finalModem = modem;
126 if (finalModem != null) {
129 checkAndStartModemIfNeeded();
134 public void initialize() {
135 updateStatus(ThingStatus.UNKNOWN);
137 ScheduledFuture<?> checkScheduledFinal = checkScheduled;
138 if (checkScheduledFinal == null || (checkScheduledFinal.isDone()) && this.shouldRun) {
139 checkScheduled = scheduler.scheduleWithFixedDelay(this::checkAndStartModemIfNeeded, 0, 15,
144 private synchronized void checkAndStartModemIfNeeded() {
146 if (shouldRun && !isRunning()) {
147 logger.debug("Initializing smsmodem");
148 // ensure the underlying modem is stopped before trying to (re)starting it :
149 Modem finalModem = modem;
150 if (finalModem != null) {
154 if (getThing().getThingTypeUID().equals(SMSModemBindingConstants.SMSMODEMBRIDGE_THING_TYPE)) {
155 SMSModemBridgeConfiguration config = getConfigAs(SMSModemBridgeConfiguration.class);
156 modem = new Modem(serialPortManager, resolveEventualSymbolicLink(config.serialPort),
157 Integer.valueOf(config.baud), config.simPin, scheduler, config.pollingInterval,
158 config.delayBetweenSend);
160 logName = config.serialPort + " | " + config.baud;
161 } else if (getThing().getThingTypeUID()
162 .equals(SMSModemBindingConstants.SMSMODEMREMOTEBRIDGE_THING_TYPE)) {
163 SMSModemRemoteBridgeConfiguration config = getConfigAs(SMSModemRemoteBridgeConfiguration.class);
164 modem = new Modem(serialPortManager, resolveEventualSymbolicLink(config.ip),
165 Integer.valueOf(config.networkPort), config.simPin, scheduler, config.pollingInterval,
166 config.delayBetweenSend);
167 checkRemoteParam(config);
168 logName = config.ip + ":" + config.networkPort;
170 throw new IllegalArgumentException("Invalid thing type");
172 logger.debug("Now trying to start SMSModem {}", logName);
174 if (finalModem != null) {
175 finalModem.registerStatusListener(this);
176 finalModem.registerMessageListener(this);
177 finalModem.registerInformationListener(this);
180 logger.debug("SMSModem {} started", logName);
182 } catch (ModemConfigurationException e) {
183 String message = e.getMessage();
184 if (e.getCause() != null && e.getCause() instanceof IOException) {
185 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message);
192 private void checkParam(SMSModemBridgeConfiguration config) throws ModemConfigurationException {
193 String realSerialPort = resolveEventualSymbolicLink(config.serialPort);
194 SerialPortIdentifier identifier = serialPortManager.getIdentifier(realSerialPort);
195 if (identifier == null) {
197 throw new ModemConfigurationException(
198 realSerialPort + " with " + config.baud + " is not a valid serial port | baud");
202 private void checkRemoteParam(SMSModemRemoteBridgeConfiguration config) throws ModemConfigurationException {
204 InetAddress inetAddress = InetAddress.getByName(config.ip);
205 String ip = inetAddress.getHostAddress();
207 // test reachable address :
208 try (Socket s = new Socket(ip, config.networkPort)) {
210 } catch (IOException | NumberFormatException ex) {
212 throw new ModemConfigurationException(
213 config.ip + ":" + config.networkPort + " is not a reachable address:port", ex);
217 private String resolveEventualSymbolicLink(String serialPortOrIp) {
218 String keepResult = serialPortOrIp;
219 Path maybePath = Paths.get(serialPortOrIp);
220 File maybeFile = maybePath.toFile();
221 if (maybeFile.exists() && Files.isSymbolicLink(maybePath)) {
223 maybePath = maybePath.toRealPath();
224 keepResult = maybePath.toAbsolutePath().toString();
225 } catch (IOException e) {
226 } // nothing to do, not a valid symbolic link, return
231 public boolean isRunning() {
232 Modem finalModem = modem;
233 return finalModem != null
234 && (finalModem.getStatus() == Status.Started || finalModem.getStatus() == Status.Starting);
238 public void handleCommand(ChannelUID channelUID, Command command) {
242 public void messageReceived(InboundMessage message) {
243 String sender = message.getOriginatorAddress().getAddress();
244 Payload payload = message.getPayload();
246 if (payload.getType().equals(Type.Text)) {
247 String text = payload.getText();
251 logger.warn("Message has no payload !");
255 byte[] bytes = payload.getBytes();
257 logger.warn("Message payload in binary format. Don't know how to handle it. Please report it.");
258 messageText = bytes.toString();
260 logger.warn("Message has no payload !");
264 logger.debug("Receiving new message from {} : {}", sender, messageText);
266 // dispatch to conversation :
267 for (SMSConversationHandler child : getChildHandlers()) {
268 child.checkAndReceive(sender, messageText);
272 String recipientAndMessage = sender + "|" + messageText;
273 triggerChannel(SMSModemBindingConstants.CHANNEL_TRIGGER_MODEM_RECEIVE, recipientAndMessage);
275 // prepare discovery service
276 senderMsisdn.add(sender);
277 final SMSConversationDiscoveryService finalDiscoveryService = discoveryService;
278 if (finalDiscoveryService != null) {
279 finalDiscoveryService.buildByAutoDiscovery(sender);
281 try { // delete message on the sim
282 Modem finalModem = modem;
283 if (finalModem != null) {
284 finalModem.delete(message);
286 } catch (CommunicationException e) {
287 logger.warn("Cannot delete message after receiving it !", e);
294 * @param recipient The recipient for the message
295 * @param text The message content
296 * @param deliveryReport If we should ask the network for a delivery report
298 public void send(String recipient, String text, boolean deliveryReport, @Nullable String encoding) {
299 OutboundMessage out = new OutboundMessage(recipient, text);
301 if (encoding != null && !encoding.isEmpty()) {
302 Encoding encoding2 = Encoding.valueOf(encoding);
303 out.setEncoding(encoding2);
305 } catch (IllegalArgumentException e) {
306 logger.warn("Encoding {} is not supported. Use Enc7, Enc8, EncUcs2, or EncCustom", encoding);
308 out.setRequestDeliveryReport(deliveryReport);
309 logger.debug("Sending message to {}", recipient);
310 Modem finalModem = modem;
311 if (finalModem != null) {
312 finalModem.queue(out);
317 * Used by the scanning discovery service to create conversation
319 * @return All senders of the received messages since the last start
321 public Set<String> getAllSender() {
322 return new HashSet<>(senderMsisdn);
326 public Collection<Class<? extends ThingHandlerService>> getServices() {
327 return Set.of(SMSModemActions.class, SMSConversationDiscoveryService.class);
331 public boolean processStatusCallback(Modem.Status oldStatus, Modem.Status newStatus) {
334 String finalDescription = "unknown";
335 Modem finalModem = modem;
336 if (finalModem != null) {
337 finalDescription = finalModem.getDescription();
339 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
340 "SMSLib reported an error on the underlying modem " + finalDescription);
343 updateStatus(ThingStatus.ONLINE);
346 updateStatus(ThingStatus.UNKNOWN);
349 if (thing.getStatus() != ThingStatus.OFFLINE) {
350 updateStatus(ThingStatus.OFFLINE);
354 if (thing.getStatus() != ThingStatus.OFFLINE) {
355 updateStatus(ThingStatus.OFFLINE);
362 public void setDiscoveryService(SMSConversationDiscoveryService smsConversationDiscoveryService) {
363 this.discoveryService = smsConversationDiscoveryService;
367 public void messageSent(OutboundMessage message) {
368 DeliveryStatus sentStatus;
369 switch (message.getSentStatus()) {
371 sentStatus = DeliveryStatus.FAILED;
375 sentStatus = DeliveryStatus.QUEUED;
378 sentStatus = DeliveryStatus.SENT;
380 default: // shoult not happened
381 sentStatus = DeliveryStatus.UNKNOWN;
384 // dispatch to conversation :
385 MsIsdn recipientAddress = message.getRecipientAddress();
386 if (recipientAddress != null) {
387 String recipient = recipientAddress.getAddress();
388 for (SMSConversationHandler child : getChildHandlers()) {
389 child.checkAndUpdateDeliveryStatus(recipient, sentStatus);
395 public void messageDelivered(DeliveryReportMessage message) {
396 DeliveryStatus sentStatus;
397 switch (message.getDeliveryStatus()) {
399 sentStatus = DeliveryStatus.DELIVERED;
403 sentStatus = DeliveryStatus.FAILED;
406 sentStatus = DeliveryStatus.EXPIRED;
409 sentStatus = DeliveryStatus.PENDING;
413 sentStatus = DeliveryStatus.UNKNOWN;
416 MsIsdn recipientAddress = message.getRecipientAddress();
417 if (recipientAddress != null) {
418 String recipient = recipientAddress.getAddress();
419 for (SMSConversationHandler child : getChildHandlers()) {
420 child.checkAndUpdateDeliveryStatus(recipient, sentStatus);
424 Modem finalModem = modem;
425 if (finalModem != null) {
426 finalModem.delete(message);
428 } catch (CommunicationException e) {
429 logger.warn("Cannot delete delivery report after receiving it !", e);
433 private Set<SMSConversationHandler> getChildHandlers() {
434 return getThing().getThings().stream().map(Thing::getHandler).filter(Objects::nonNull)
435 .map(handler -> (SMSConversationHandler) handler).collect(Collectors.toSet());
439 public void setManufacturer(String manufacturer) {
440 thing.setProperty(SMSModemBindingConstants.PROPERTY_MANUFACTURER, manufacturer);
444 public void setModel(String model) {
445 thing.setProperty(SMSModemBindingConstants.PROPERTY_MODEL, model);
449 public void setSwVersion(String swVersion) {
450 thing.setProperty(SMSModemBindingConstants.PROPERTY_SWVERSION, swVersion);
454 public void setSerialNo(String serialNo) {
455 thing.setProperty(SMSModemBindingConstants.PROPERTY_SERIALNO, serialNo);
459 public void setImsi(String imsi) {
460 thing.setProperty(SMSModemBindingConstants.PROPERTY_IMSI, imsi);
464 public void setRssi(String rssi) {
465 thing.setProperty(SMSModemBindingConstants.PROPERTY_RSSI, rssi);
469 public void setMode(String mode) {
470 thing.setProperty(SMSModemBindingConstants.PROPERTY_MODE, mode);
474 public void setTotalSent(String totalSent) {
475 thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALSENT, totalSent);
479 public void setTotalFailed(String totalFailed) {
480 thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALFAILED, totalFailed);
484 public void setTotalReceived(String totalReceived) {
485 thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALRECEIVED, totalReceived);
489 public void setTotalFailures(String totalFailure) {
490 thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALFAILURE, totalFailure);