pysms

changeset 1:f2d4cc3e5eac tip

Now with bling bling!
author Tobias Mueller (xbox) <muelli@auftrags-killer.org>
date Sun, 04 Oct 2009 21:50:21 +0100
parents 2d5b10978ce7
children
files send_sms.py
diffstat 1 files changed, 209 insertions(+), 14 deletions(-) [+]
line diff
     1.1 --- a/send_sms.py	Sat Oct 03 19:53:52 2009 +0100
     1.2 +++ b/send_sms.py	Sun Oct 04 21:50:21 2009 +0100
     1.3 @@ -1,40 +1,235 @@
     1.4  #!/usr/bin/env python
     1.5  # -*- coding: utf8 -*-
     1.6 +import logging
     1.7 +import os
     1.8  import serial
     1.9 +import sys
    1.10  import time
    1.11  
    1.12  DEVICE = '/dev/ttyACM0'
    1.13  
    1.14 -def send_sms(nr, msg):
    1.15 -    ser = serial.Serial(DEVICE, 115200, timeout=1)
    1.16 +log = logging.getLogger()
    1.17 +
    1.18 +def parse_o2sms_config(config):
    1.19 +    '''Parses a given string and returns a dict with its values, i.e.
    1.20 +    >>> parse_o2sms_config("alias foo +353123")
    1.21 +    {'Contacts': {'foo': ('+353123',)}}
    1.22 +    >>> parse_o2sms_config("alias \t foo\t+353123")
    1.23 +    {'Contacts': {'foo': ('+353123',)}}
    1.24 +    >>> parse_o2sms_config("alias foo +353123 +353456")
    1.25 +    {'Contacts': {'foo': ('+353123', '+353456')}}
    1.26 +    >>> parse_o2sms_config("alias foo +353123 bar")
    1.27 +    {'Contacts': {'foo': ('+353123', 'bar')}}
    1.28 +    >>> parse_o2sms_config("""username foo
    1.29 +    ...                       password\tbar""")
    1.30 +    {'username': 'foo', 'password': 'bar', 'Contacts': {}}
    1.31 +    '''
    1.32 +    config = config.strip()
    1.33 +    '''    >
    1.34 +    '''
    1.35 +    parsed = {}
    1.36 +    contacts = {}
    1.37 +    for line in config.splitlines():
    1.38 +        log.debug('Looking at line: %s', line)
    1.39 +        if not line.startswith('#'):
    1.40 +            tokens = [t.strip() for t in line.strip().split()]
    1.41 +            
    1.42 +            if len(tokens) == 1:
    1.43 +                log.critical("Dunno what has only one token: %s", tokens)
    1.44 +            elif len(tokens) == 2:
    1.45 +                key, value = tokens
    1.46 +                if not key == 'alias':
    1.47 +                    parsed[key] = value
    1.48 +            elif len(tokens) >= 3:
    1.49 +                log.debug("We must have found an alias! %s", tokens)
    1.50 +                assert tokens[0] == 'alias'
    1.51 +                key, values = tokens[1], tokens[2:]
    1.52 +                contacts[key] = tuple(values)
    1.53 +                
    1.54 +    parsed['Contacts'] = contacts
    1.55 +    return parsed
    1.56 +
    1.57 +def parse_o2sms_config_file(fname="~/.o2sms/config"):
    1.58 +    config_path = os.path.expanduser(fname)
    1.59 +    config = file(config_path, 'r').read()
    1.60 +    return parse_o2sms_config(config)
    1.61 +
    1.62 +def send_sms(nr, msg, device='/dev/ttyACM0'):
    1.63 +    log.debug('Sending SMS to %s', nr)
    1.64 +    ser = serial.Serial(device, 115200, timeout=1)
    1.65 +    assert ser.isOpen()
    1.66      ser.write('AT\r')
    1.67      line = ser.readline()
    1.68 -    print line
    1.69 +    log.debug('RECV: %s', line)
    1.70      line = ser.readline()
    1.71 -    print line
    1.72 +    log.debug('RECV: %s', line)
    1.73      assert line == "OK\r\n"
    1.74 -    
    1.75 +    log.debug('Sucessfully opened device')
    1.76      ser.write('AT+CMGF=1\r')
    1.77      line = ser.readline()
    1.78 +    log.debug('RECV: %s', line)
    1.79      line = ser.readline()
    1.80 +    log.debug('RECV: %s', line)
    1.81      assert line == "OK\r\n"
    1.82 -    
    1.83 +   
    1.84      ser.write('AT+CMGS="%s"\r' % nr)
    1.85 -    ser.write('%s\n' % msg)
    1.86 +    ser.write('%s' % msg)
    1.87      ser.write(chr(26))
    1.88      time.sleep(3)
    1.89      lines = ser.readlines()   #read a ā€˜\n’ terminated line
    1.90 -    print lines
    1.91 -    ser.close()
    1.92 +    log.debug('RECV: %s', lines)
    1.93 +    if any([line.startswith('ERROR') for line in lines]):
    1.94 +        log.critical('Reveiced an Error!\n%s', lines)
    1.95 +    #ser.close()
    1.96 +    log.debug('Sucessfully sent message!')
    1.97 +
    1.98      
    1.99 +def is_phone_number(number):
   1.100 +    '''Determines whether a given string looks like a fully qualified phone number
   1.101 +    >>> is_phone_number('+1234')
   1.102 +    True
   1.103 +    >>> is_phone_number('foo')
   1.104 +    False
   1.105 +    >>> is_phone_number('123')
   1.106 +    False
   1.107 +    >>> is_phone_number('+123sab')
   1.108 +    False
   1.109 +    '''
   1.110 +    return number[0] == '+' and number[1:].isdigit()
   1.111 +
   1.112 +def resolve_candidate(cand, config=None):
   1.113 +    ''' Tries to resolve a given string by deciding whether it's a well 
   1.114 +    formed number or looking it (recursively) up in the contacts
   1.115 +    >>> resolve_candidate('+353123')
   1.116 +    ('+353123',)
   1.117 +    >>> resolve_candidate('08123')
   1.118 +    ('+3538123',)
   1.119 +    >>> config = {'Contacts':{'foo1':('+353123',)}}
   1.120 +    >>> resolve_candidate('foo1', config)
   1.121 +    ('+353123',)
   1.122 +    >>> config = {'Contacts':{'bar':('+353456',), 'foo2':('+353123','bar')}}
   1.123 +    >>> cands = resolve_candidate('foo2', config)
   1.124 +    >>> '+353123' in cands
   1.125 +    True
   1.126 +    >>> '+353456' in cands
   1.127 +    True
   1.128 +    '''
   1.129 +    if cand[0].isalpha():
   1.130 +        'Probably a name, so load config'
   1.131 +        if not config:
   1.132 +            config = parse_o2sms_config_file()
   1.133 +        contacts = config['Contacts']
   1.134 +        numbers = contacts[cand]
   1.135 +        candidates = set()
   1.136 +        for number in numbers:
   1.137 +            if not is_phone_number(number):
   1.138 +                log.debug('%s is not a fully qualified number, so try to recursively look that up', number)
   1.139 +                resolved_numbers = resolve_candidate(number, config)
   1.140 +                for resolved_number in resolved_numbers:
   1.141 +                    candidates.add(resolved_number)
   1.142 +            else:
   1.143 +                candidates.add(number)
   1.144 +        candidates = tuple(candidates)
   1.145 +        
   1.146 +    elif is_phone_number(cand):
   1.147 +        candidates = (cand,)
   1.148 +    elif cand.startswith('08'):
   1.149 +        candidates = ('+353' + cand[1:],)
   1.150 +    else:
   1.151 +        log.critical('Dunno what to do with candidate %s. Is it in your local numbers? Why doesnt start it with a + or 08?', cand)
   1.152 +        candidates = tuple()
   1.153 +    return candidates
   1.154 +    
   1.155 +
   1.156 +def split_string(message, threshold=160):
   1.157 +    '''Splits a string every $threshold characters
   1.158 +    >>> len(list(split_string('x'*150, 160)))
   1.159 +    1
   1.160 +    >>> len(list(split_string('x'*150, 60)))
   1.161 +    3
   1.162 +    >>> for m in split_string('x', 160): print m
   1.163 +    x
   1.164 +    >>> for m in split_string('12', 1): print m
   1.165 +    1
   1.166 +    2
   1.167 +    '''
   1.168 +    m = message
   1.169 +    while len(m) > 0:
   1.170 +        yield m[:threshold]
   1.171 +        m = m[threshold:]
   1.172 +    
   1.173 +
   1.174 +def is_o2_number(number):
   1.175 +    '''Tries to determine whether a number is from O2Ireland by just
   1.176 +    looking at it's prefix. Pretty lame but still...
   1.177 +    >>> is_o2_number('086123')
   1.178 +    True
   1.179 +    >>> is_o2_number('+35386123')
   1.180 +    True
   1.181 +    >>> is_o2_number('087123')
   1.182 +    False
   1.183 +    >>> is_o2_number('+35387123')
   1.184 +    False
   1.185 +    >>> is_o2_number('')
   1.186 +    False
   1.187 +    >>> is_o2_number('foo')
   1.188 +    False
   1.189 +    '''
   1.190 +    normalized = resolve_candidate(number)
   1.191 +    return normalized.startswith('+35386')
   1.192 +
   1.193  if __name__ == "__main__":
   1.194      import optparse
   1.195      parser = optparse.OptionParser(
   1.196 -         usage = "%prog number message",
   1.197 +         usage = "%prog number",
   1.198           description = "Simple SMS Interface"
   1.199      )
   1.200 +
   1.201 +    parser.add_option("-d", "--device",
   1.202 +                      dest="device", default="/dev/rfcomm2",
   1.203 +                      help="Use this serial device to send the SMS, you might want to add it to /etc/bluetooth/rfcomm")
   1.204 +    parser.add_option("-l", "--loglevel", dest="loglevel",
   1.205 +                      help="Sets the loglevel to one of debug, info, warn, error, critical",
   1.206 +                      default="warn")
   1.207 +    parser.add_option("-m", "--message",
   1.208 +                      dest="message", default=None,
   1.209 +                      help="Use this message and do not wait for stdin")
   1.210 +    parser.add_option("-t", "--test",
   1.211 +                      action="store_true", dest="test", default=False,
   1.212 +                      help="Run Tests")
   1.213      (options, args) = parser.parse_args()
   1.214 -    
   1.215 -    nr, msg = args[0], args[1]
   1.216 -    
   1.217 -    send_sms(nr, msg)
   1.218 +    device = options.device
   1.219 +
   1.220 +    loglevel = {'debug': logging.DEBUG, 'info': logging.INFO,
   1.221 +                'warn': logging.WARN, 'error': logging.ERROR,
   1.222 +                'critical': logging.CRITICAL}.get(options.loglevel, "warn")
   1.223 +    logging.basicConfig(level=loglevel)
   1.224 +    log = logging.getLogger("SMS Main")
   1.225 +    log.debug('running with %s', options)
   1.226 +    test = options.test
   1.227 +    if test:
   1.228 +        import doctest
   1.229 +        doctest.testmod()
   1.230 +        sys.exit(0)
   1.231 +
   1.232 +    if not len(args) > 0:
   1.233 +        parser.error('Please specify at least one number')
   1.234 +        '''Will bailout here automatically'''
   1.235 +    candidates = args
   1.236 +    numbers = set()
   1.237 +    for cand in candidates:
   1.238 +        cands = resolve_candidate(cand)
   1.239 +        numbers.add(cands)
   1.240 +    if options.message is None:
   1.241 +        log.info('Trying to read from stdin')
   1.242 +        message = sys.stdin.read().strip()
   1.243 +    else:
   1.244 +        message = options.message
   1.245 +        log.debug('Taking message from argument: len %d', len(message))
   1.246 +        
   1.247 +    for number in numbers:
   1.248 +        msgs = tuple(split_string(message))
   1.249 +        log.info('Message has %d chars and is weigths %d sms', len(message), len(msgs))
   1.250 +        for msg in msgs:
   1.251 +            send_sms(number, msg.strip(), device)
   1.252 +            #time.sleep(5)