#!/usr/bin/env python3 # -*- Mode: python; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2 of the License, or (at your option) any # later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License along # with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # Copyright (C) 2016 Aleksander Morgado # Copyright (C) 2025 Dan Williams # import os import signal import sys watchers = {} import gi gi.require_version('ModemManager', '1.0') from gi.repository import Gio, GLib, GObject, ModemManager sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'modem-watcher-python')) import ModemWatcher class SmsWatcher: """ The SmsWatcher class watches for and prints complete SMS messages. """ def __init__(self, sms): self.sms = sms # Connect to the SMS message's 'state' property to know when all parts # have been received. self.state_id = self.sms.connect('notify::state', self.on_state_changed) self.on_state_changed(sms, sms.get_state()) def cleanup(self): self.sms.disconnect(self.state_id) self.state_id = 0 def on_state_changed(self, sms, prop): # When all parts have been received, print the SMS if sms.get_state() == ModemManager.SmsState.RECEIVED: self.show() def show(self): print('[SmsWatcher] PATH: %s' % self.sms.get_path()) print('[SmsWatcher] NUMBER: %s' % self.sms.get_number()) print('[SmsWatcher] TEXT: \'%s\'' % self.sms.get_text()) print('[SmsWatcher] PDU-TYPE: %s' % ModemManager.SmsPduType.get_string(self.sms.get_pdu_type())) print('[SmsWatcher] STATE: %s' % ModemManager.SmsState.get_string(self.sms.get_state())) if self.sms.get_validity_type() == ModemManager.SmsValidityType.RELATIVE: print('[SmsWatcher] VALIDITY: %u' % self.sms.get_validity_relative()) print('[SmsWatcher] STORAGE: %s' % ModemManager.SmsStorage.get_string(self.sms.get_storage())) print('[SmsWatcher] SMSC: %s' % self.sms.get_smsc()) if self.sms.get_class() >= 0: print('[SmsWatcher] STORAGE: %d' % self.sms.get_class()) if self.sms.get_pdu_type() == ModemManager.SmsPduType.SUBMIT: delivery_report = "not requested" if self.sms.get_delivery_report_request(): delivery_report = "requested" print('[SmsWatcher] DELIVERY-REPORT: %s' % delivery_report) if self.sms.get_message_reference() != 0: print('[SmsWatcher] MSG-REFERENCE: %u' % self.sms.get_message_reference()) print('[SmsWatcher] TIMESTAMP: %s' % self.sms.get_timestamp()) if self.sms.get_delivery_state () != ModemManager.SmsDeliveryState.UNKNOWN: print('[SmsWatcher] DELIVERY-STATE: %s' % ModemManager.SmsDeliveryState.get_string_extended (self.sms.get_delivery_state())) if self.sms.get_discharge_timestamp() is not None: print('[SmsWatcher] DISCHARGE-TIMESTAMP: %s' % self.sms.get_discharge_timestamp()) print('') class MessagingWatcher: """ The MessagingWatcher class monitors a modem's Messaging interface for SMS messages. """ def __init__(self, messaging): self.iface = messaging # Connect to the messaging interface's added/removed signals to be # notified when SMS messages are received or deleted self.sms_added_id = self.iface.connect('added', self.on_sms_added) self.sms_removed_id = self.iface.connect('deleted', self.on_sms_removed) # List all existing messages self.messages = {} for sms in self.iface.list_sync(): self.messages[sms.get_path()] = SmsWatcher(sms) def cleanup(self): # We no longer care about signals; disconnect them self.iface.disconnect(self.sms_added_id) self.sms_added_id = 0 self.iface.disconnect(self.sms_removed_id) self.sms_removed_id = 0 # Clean up each SMS we're tracking for sms in self.messages.values(): sms.cleanup() self.messages = {} def on_sms_added(self, messaging, path, received): # MM/libmm-glib do not yet provide a way to retrieve # a single SMS object given its D-Bus path. List them # and find the one we want. for sms in self.iface.list_sync(): if sms.get_path() == path: # Watch this SMS self.messages[sms.get_path()] = SmsWatcher(sms) def on_sms_removed(self, messaging, path): try: # Clean up the SMS and stop tracking it sms_watcher = self.messages[path] sms_watcher.cleanup() del self.messages[path] except KeyError: pass def modem_callback(obj, added): global watchers if added: # Get the messaging interface of this modem and start watching for SMSes messaging = obj.get_modem_messaging() if messaging is not None: watchers[obj] = MessagingWatcher(messaging) else: try: # Modem went away; clean up watcher = watchers[obj] watcher.cleanup() del(watchers, obj) except KeyError: pass def signal_handler(loop): """SIGHUP and SIGINT handler.""" loop.quit() def main(): """Main routine.""" modem_watcher = ModemWatcher.ModemWatcher(modem_callback) # Main loop to watch for incoming SMSes main_loop = GLib.MainLoop() GLib.unix_signal_add( GLib.PRIORITY_HIGH, signal.SIGHUP, signal_handler, main_loop) GLib.unix_signal_add( GLib.PRIORITY_HIGH, signal.SIGTERM, signal_handler, main_loop) try: main_loop.run() except KeyboardInterrupt: pass if __name__ == "__main__": main()