]> git.basschouten.com Git - openhab-addons.git/blob
3092ee06c1926c7d731d71285c65c88d09675b50
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.smsmodem.internal.handler;
14
15 import java.io.File;
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;
25 import java.util.Set;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.stream.Collectors;
29
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;
64
65 /**
66  * The {@link SMSModemBridgeHandler} is responsible for handling
67  * communication with the modem.
68  *
69  * @author Gwendal ROULLEAU - Initial contribution
70  */
71 @NonNullByDefault
72 public class SMSModemBridgeHandler extends BaseBridgeHandler
73         implements IModemStatusListener, IInboundOutboundMessageListener, IDeviceInformationListener {
74
75     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(
76             SMSModemBindingConstants.SMSMODEMBRIDGE_THING_TYPE,
77             SMSModemBindingConstants.SMSMODEMREMOTEBRIDGE_THING_TYPE);
78
79     private final Logger logger = LoggerFactory.getLogger(SMSModemBridgeHandler.class);
80
81     private SerialPortManager serialPortManager;
82
83     /**
84      * The smslib object responsible for the serial communication with the modem
85      */
86     private @Nullable Modem modem;
87
88     /**
89      * A scheduled watchdog check
90      */
91     private @Nullable ScheduledFuture<?> checkScheduled;
92
93     // we keep a list of msisdn sender for autodiscovery
94     private Set<String> senderMsisdn = new HashSet<String>();
95     private @Nullable SMSConversationDiscoveryService discoveryService;
96
97     private boolean shouldRun = false;
98
99     public SMSModemBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) {
100         super(bridge);
101         this.serialPortManager = serialPortManager;
102     }
103
104     @Override
105     public void dispose() {
106         shouldRun = false;
107         ScheduledFuture<?> checkScheduledFinal = checkScheduled;
108         if (checkScheduledFinal != null) {
109             checkScheduledFinal.cancel(true);
110         }
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);
117         }
118         modem = null;
119     }
120
121     @Override
122     protected void updateConfiguration(Configuration configuration) {
123         super.updateConfiguration(configuration);
124         scheduler.execute(() -> {
125             Modem finalModem = modem;
126             if (finalModem != null) {
127                 finalModem.stop();
128             }
129             checkAndStartModemIfNeeded();
130         });
131     }
132
133     @Override
134     public void initialize() {
135         updateStatus(ThingStatus.UNKNOWN);
136         shouldRun = true;
137         ScheduledFuture<?> checkScheduledFinal = checkScheduled;
138         if (checkScheduledFinal == null || (checkScheduledFinal.isDone()) && this.shouldRun) {
139             checkScheduled = scheduler.scheduleWithFixedDelay(this::checkAndStartModemIfNeeded, 0, 15,
140                     TimeUnit.SECONDS);
141         }
142     }
143
144     private synchronized void checkAndStartModemIfNeeded() {
145         try {
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) {
151                     finalModem.stop();
152                 }
153                 String logName;
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);
159                     checkParam(config);
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;
169                 } else {
170                     throw new IllegalArgumentException("Invalid thing type");
171                 }
172                 logger.debug("Now trying to start SMSModem {}", logName);
173                 finalModem = modem;
174                 if (finalModem != null) {
175                     finalModem.registerStatusListener(this);
176                     finalModem.registerMessageListener(this);
177                     finalModem.registerInformationListener(this);
178                     finalModem.start();
179                 }
180                 logger.debug("SMSModem {} started", logName);
181             }
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);
186             } else {
187                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message);
188             }
189         }
190     }
191
192     private void checkParam(SMSModemBridgeConfiguration config) throws ModemConfigurationException {
193         String realSerialPort = resolveEventualSymbolicLink(config.serialPort);
194         SerialPortIdentifier identifier = serialPortManager.getIdentifier(realSerialPort);
195         if (identifier == null) {
196             // no serial port
197             throw new ModemConfigurationException(
198                     realSerialPort + " with " + config.baud + " is not a valid serial port | baud");
199         }
200     }
201
202     private void checkRemoteParam(SMSModemRemoteBridgeConfiguration config) throws ModemConfigurationException {
203         try {
204             InetAddress inetAddress = InetAddress.getByName(config.ip);
205             String ip = inetAddress.getHostAddress();
206
207             // test reachable address :
208             try (Socket s = new Socket(ip, config.networkPort)) {
209             }
210         } catch (IOException | NumberFormatException ex) {
211             // no ip
212             throw new ModemConfigurationException(
213                     config.ip + ":" + config.networkPort + " is not a reachable address:port", ex);
214         }
215     }
216
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)) {
222             try {
223                 maybePath = maybePath.toRealPath();
224                 keepResult = maybePath.toAbsolutePath().toString();
225             } catch (IOException e) {
226             } // nothing to do, not a valid symbolic link, return
227         }
228         return keepResult;
229     }
230
231     public boolean isRunning() {
232         Modem finalModem = modem;
233         return finalModem != null
234                 && (finalModem.getStatus() == Status.Started || finalModem.getStatus() == Status.Starting);
235     }
236
237     @Override
238     public void handleCommand(ChannelUID channelUID, Command command) {
239     }
240
241     @Override
242     public void messageReceived(InboundMessage message) {
243         String sender = message.getOriginatorAddress().getAddress();
244         Payload payload = message.getPayload();
245         String messageText;
246         if (payload.getType().equals(Type.Text)) {
247             String text = payload.getText();
248             if (text != null) {
249                 messageText = text;
250             } else {
251                 logger.warn("Message has no payload !");
252                 return;
253             }
254         } else {
255             byte[] bytes = payload.getBytes();
256             if (bytes != null) {
257                 logger.warn("Message payload in binary format. Don't know how to handle it. Please report it.");
258                 messageText = bytes.toString();
259             } else {
260                 logger.warn("Message has no payload !");
261                 return;
262             }
263         }
264         logger.debug("Receiving new message from {} : {}", sender, messageText);
265
266         // dispatch to conversation :
267         for (SMSConversationHandler child : getChildHandlers()) {
268             child.checkAndReceive(sender, messageText);
269         }
270
271         // channel trigger
272         String recipientAndMessage = sender + "|" + messageText;
273         triggerChannel(SMSModemBindingConstants.CHANNEL_TRIGGER_MODEM_RECEIVE, recipientAndMessage);
274
275         // prepare discovery service
276         senderMsisdn.add(sender);
277         final SMSConversationDiscoveryService finalDiscoveryService = discoveryService;
278         if (finalDiscoveryService != null) {
279             finalDiscoveryService.buildByAutoDiscovery(sender);
280         }
281         try { // delete message on the sim
282             Modem finalModem = modem;
283             if (finalModem != null) {
284                 finalModem.delete(message);
285             }
286         } catch (CommunicationException e) {
287             logger.warn("Cannot delete message after receiving it !", e);
288         }
289     }
290
291     /**
292      * Send message
293      *
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
297      */
298     public void send(String recipient, String text, boolean deliveryReport, @Nullable String encoding) {
299         OutboundMessage out = new OutboundMessage(recipient, text);
300         try {
301             if (encoding != null && !encoding.isEmpty()) {
302                 Encoding encoding2 = Encoding.valueOf(encoding);
303                 out.setEncoding(encoding2);
304             }
305         } catch (IllegalArgumentException e) {
306             logger.warn("Encoding {} is not supported. Use Enc7, Enc8, EncUcs2, or EncCustom", encoding);
307         }
308         out.setRequestDeliveryReport(deliveryReport);
309         logger.debug("Sending message to {}", recipient);
310         Modem finalModem = modem;
311         if (finalModem != null) {
312             finalModem.queue(out);
313         }
314     }
315
316     /**
317      * Used by the scanning discovery service to create conversation
318      *
319      * @return All senders of the received messages since the last start
320      */
321     public Set<String> getAllSender() {
322         return new HashSet<>(senderMsisdn);
323     }
324
325     @Override
326     public Collection<Class<? extends ThingHandlerService>> getServices() {
327         return Set.of(SMSModemActions.class, SMSConversationDiscoveryService.class);
328     }
329
330     @Override
331     public boolean processStatusCallback(Modem.Status oldStatus, Modem.Status newStatus) {
332         switch (newStatus) {
333             case Error:
334                 String finalDescription = "unknown";
335                 Modem finalModem = modem;
336                 if (finalModem != null) {
337                     finalDescription = finalModem.getDescription();
338                 }
339                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
340                         "SMSLib reported an error on the underlying modem " + finalDescription);
341                 break;
342             case Started:
343                 updateStatus(ThingStatus.ONLINE);
344                 break;
345             case Starting:
346                 updateStatus(ThingStatus.UNKNOWN);
347                 break;
348             case Stopped:
349                 if (thing.getStatus() != ThingStatus.OFFLINE) {
350                     updateStatus(ThingStatus.OFFLINE);
351                 }
352                 break;
353             case Stopping:
354                 if (thing.getStatus() != ThingStatus.OFFLINE) {
355                     updateStatus(ThingStatus.OFFLINE);
356                 }
357                 break;
358         }
359         return false;
360     }
361
362     public void setDiscoveryService(SMSConversationDiscoveryService smsConversationDiscoveryService) {
363         this.discoveryService = smsConversationDiscoveryService;
364     }
365
366     @Override
367     public void messageSent(OutboundMessage message) {
368         DeliveryStatus sentStatus;
369         switch (message.getSentStatus()) {
370             case Failed:
371                 sentStatus = DeliveryStatus.FAILED;
372                 break;
373             case Unsent:
374             case Queued:
375                 sentStatus = DeliveryStatus.QUEUED;
376                 break;
377             case Sent:
378                 sentStatus = DeliveryStatus.SENT;
379                 break;
380             default: // shoult not happened
381                 sentStatus = DeliveryStatus.UNKNOWN;
382                 break;
383         }
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);
390             }
391         }
392     }
393
394     @Override
395     public void messageDelivered(DeliveryReportMessage message) {
396         DeliveryStatus sentStatus;
397         switch (message.getDeliveryStatus()) {
398             case Delivered:
399                 sentStatus = DeliveryStatus.DELIVERED;
400                 break;
401             case Error:
402             case Failed:
403                 sentStatus = DeliveryStatus.FAILED;
404                 break;
405             case Expired:
406                 sentStatus = DeliveryStatus.EXPIRED;
407                 break;
408             case Pending:
409                 sentStatus = DeliveryStatus.PENDING;
410                 break;
411             case Unknown:
412             default:
413                 sentStatus = DeliveryStatus.UNKNOWN;
414                 break;
415         }
416         MsIsdn recipientAddress = message.getRecipientAddress();
417         if (recipientAddress != null) {
418             String recipient = recipientAddress.getAddress();
419             for (SMSConversationHandler child : getChildHandlers()) {
420                 child.checkAndUpdateDeliveryStatus(recipient, sentStatus);
421             }
422         }
423         try {
424             Modem finalModem = modem;
425             if (finalModem != null) {
426                 finalModem.delete(message);
427             }
428         } catch (CommunicationException e) {
429             logger.warn("Cannot delete delivery report after receiving it !", e);
430         }
431     }
432
433     private Set<SMSConversationHandler> getChildHandlers() {
434         return getThing().getThings().stream().map(Thing::getHandler).filter(Objects::nonNull)
435                 .map(handler -> (SMSConversationHandler) handler).collect(Collectors.toSet());
436     }
437
438     @Override
439     public void setManufacturer(String manufacturer) {
440         thing.setProperty(SMSModemBindingConstants.PROPERTY_MANUFACTURER, manufacturer);
441     }
442
443     @Override
444     public void setModel(String model) {
445         thing.setProperty(SMSModemBindingConstants.PROPERTY_MODEL, model);
446     }
447
448     @Override
449     public void setSwVersion(String swVersion) {
450         thing.setProperty(SMSModemBindingConstants.PROPERTY_SWVERSION, swVersion);
451     }
452
453     @Override
454     public void setSerialNo(String serialNo) {
455         thing.setProperty(SMSModemBindingConstants.PROPERTY_SERIALNO, serialNo);
456     }
457
458     @Override
459     public void setImsi(String imsi) {
460         thing.setProperty(SMSModemBindingConstants.PROPERTY_IMSI, imsi);
461     }
462
463     @Override
464     public void setRssi(String rssi) {
465         thing.setProperty(SMSModemBindingConstants.PROPERTY_RSSI, rssi);
466     }
467
468     @Override
469     public void setMode(String mode) {
470         thing.setProperty(SMSModemBindingConstants.PROPERTY_MODE, mode);
471     }
472
473     @Override
474     public void setTotalSent(String totalSent) {
475         thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALSENT, totalSent);
476     }
477
478     @Override
479     public void setTotalFailed(String totalFailed) {
480         thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALFAILED, totalFailed);
481     }
482
483     @Override
484     public void setTotalReceived(String totalReceived) {
485         thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALRECEIVED, totalReceived);
486     }
487
488     @Override
489     public void setTotalFailures(String totalFailure) {
490         thing.setProperty(SMSModemBindingConstants.PROPERTY_TOTALFAILURE, totalFailure);
491     }
492 }