Require ~/.aprsd/config.yml

This patch completes the migration to using a config.yml file.
~/.aprsd/config.yml is now required and all options for callsign,
imap, aprs user, passwords are in the config.  If there is no existing
~/.aprsd/config.yml file, then the app will output a sample config
and exit.

This patch also adds a global logging facility that allows logging all
commands to aprsd.log as well as stdout.  You can disable logging to
stdout by adding --quiet on the command line.  You can specify the log
level with --loglevel INFO.  By default the log level is DEBUG.

This patch also updates some formatting issues and small refactoring
to ensure that the logging facility and config is read prior to starting
any network connections and/or services.
This commit is contained in:
Walter A. Boring IV 2018-11-29 08:22:41 -05:00
parent 4c8d9c3b2c
commit ce7a30aa78
2 changed files with 439 additions and 371 deletions

View File

@ -1,9 +1,10 @@
#!/usr/bin/python -u
#
# Listen on amateur radio aprs-is network for messages and respond to them.
# You must have an amateur radio callsign to use this software. Put your
# callsign in the "USER" variable and update your aprs-is password in "PASS".
# You must also have an imap email account available for polling.
# You must have an amateur radio callsign to use this software. You must
# create an ~/.aprsd/config.yml file with all of the required settings. To
# generate an example config.yml, just run aprsd, then copy the sample config
# to ~/.aprsd/config.yml and edit the settings.
#
# APRS messages:
# l(ocation) = descriptive location of calling station
@ -21,23 +22,24 @@
# python included libs
import argparse
import json
import urllib
import sys
import os
import telnetlib
import time
import re
from random import randint
import smtplib
from email.mime.text import MIMEText
import subprocess
import datetime
import calendar
import email
import threading
import signal
import json
import logging
import os
import pprint
import re
import signal
import smtplib
import subprocess
import sys
import telnetlib
import threading
import time
import urllib
from email.mime.text import MIMEText
from logging.handlers import RotatingFileHandler
# external lib imports
from imapclient import IMAPClient, SEEN
@ -46,87 +48,60 @@ from imapclient import IMAPClient, SEEN
from aprsd.fuzzyclock import fuzzy
import utils
# setup the global logger
LOG = logging.getLogger('APRSD')
# global for the config yaml
CONFIG = None
# localization, please edit:
HOST = "noam.aprs2.net" # north america tier2 servers round robin
USER = "KM6XXX-9" # callsign of this aprs client with SSID
PASS = "99999" # google how to generate this
BASECALLSIGN = "KM6XXX" # callsign of radio in the field to which we send email
shortcuts = {
"aa" : "5551239999@vtext.com",
"cl" : "craiglamparter@somedomain.org",
"wb" : "5553909472@vtext.com"
}
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
# USER = "KM6XXX-9" # callsign of this aprs client with SSID
# PASS = "99999" # google how to generate this
# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to which we send email
# shortcuts = {
# "aa" : "5551239999@vtext.com",
# "cl" : "craiglamparter@somedomain.org",
# "wb" : "5553909472@vtext.com"
# }
# globals - tell me a better way to update data being used by threads
email_sent_dict = {} # message_number:time combos so we don't resend the same email in five mins {int:int}
ack_dict = {} # message_nubmer:ack combos so we stop sending a message after an ack from radio {int:int}
message_number = 0 # current aprs radio message number, increments for each message we send over rf {int}
# global telnet connection object
tn = None
# command line args
parser = argparse.ArgumentParser()
parser.add_argument("--user",
metavar="<user>",
default=utils.env("APRS_USER"),
help="The callsign of this ARPS client with SSID"
" Default=env[APRS_USER]")
parser.add_argument("--host",
metavar="<host>",
default=utils.env("APRS_HOST"),
help="The aprs host to use Default=env[APRS_HOST]")
parser.add_argument("--password",
metavar="<password>",
default=utils.env("APRS_PASSWORD"),
help="The aprs password Default=env[APRS_PASSWORD]")
parser.add_argument("--callsign",
metavar="<callsign>",
default=utils.env("APRS_CALLSIGN"),
help="The callsign of radio in the field to which we send "
"email Default=env[APRS_CALLSIGN]")
parser.add_argument("--loglevel",
default='DEBUG',
choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'],
help="The log level to use for aprsd.log")
parser.add_argument("--quiet",
action='store_true',
help="Don't log to stdout")
args = parser.parse_args()
if not args.user:
print("Missing the aprs user (env[APRS_USER])")
parser.print_help()
parser.exit()
else:
USER = args.user
if not args.password:
print("Missing the aprs password (env[APRS_PASSWORD])")
parser.print_help()
parser.exit()
else:
PASS = args.password
if not args.callsign:
print("Missing the aprs callsign (env[APRS_CALLSIGN])")
parser.print_help()
parser.exit()
else:
BASECALLSIGN = args.callsign
# Now read the ~/.aprds/config.yml
config = utils.get_config()
if 'shortcuts' in config:
shortcuts = config['shortcuts']
else:
print("missing 'shortcuts' section of config.yml")
sys.exit(-1)
try:
tn = telnetlib.Telnet(HOST, 14580)
except Exception, e:
print "Telnet session failed.\n"
sys.exit(-1)
def setup_connection():
global tn
host = CONFIG['aprs']['host']
LOG.debug("Setting up telnet connection to '%s'" % host)
try:
tn = telnetlib.Telnet(host, 14580)
except Exception, e:
LOG.critical("Telnet session failed.\n", e)
sys.exit(-1)
def signal_handler(signal, frame):
print("Ctrl+C, exiting.")
#sys.exit(0) # thread ignores this
os._exit(0)
signal.signal(signal.SIGINT, signal_handler)
LOG.info("Ctrl+C, exiting.")
#sys.exit(0) # thread ignores this
os._exit(0)
### end signal_handler
def parse_email(msgid, data, server):
@ -172,107 +147,116 @@ def parse_email(msgid, data, server):
def resend_email(count):
date = datetime.datetime.now()
month = date.strftime("%B")[:3] # Nov, Mar, Apr
day = date.day
year = date.year
today = str(day) + "-" + month + "-" + str(year)
date = datetime.datetime.now()
month = date.strftime("%B")[:3] # Nov, Mar, Apr
day = date.day
year = date.year
today = str(day) + "-" + month + "-" + str(year)
global shortcuts
shortcuts_inverted = dict([[v,k] for k,v in shortcuts.items()]) # swap key/value
shortcuts = CONFIG['shortcuts']
shortcuts_inverted = dict([[v,k] for k,v in shortcuts.items()]) # swap key/value
server = IMAPClient('imap.yourdomain.com', use_uid=True)
server.login('KM6XXX@yourdomain.org', 'yourpassword')
select_info = server.select_folder('INBOX')
LOG.debug("resend_email: Connect to IMAP host '%s' with user '%s'" %
(CONFIG['imap']['host'],
CONFIG['imap']['login']))
server = IMAPClient(CONFIG['imap']['host'], use_uid=True)
server.login(CONFIG['imap']['login'], CONFIG['imap']['password'])
# select_info = server.select_folder('INBOX')
messages = server.search(['SINCE', today])
#print("%d messages received today" % len(messages))
messages = server.search(['SINCE', today])
LOG.debug("%d messages received today" % len(messages))
msgexists = False
msgexists = False
messages.sort(reverse=True)
del messages[int(count):] # only the latest "count" messages
for message in messages:
for msgid, data in list(server.fetch(message, ['ENVELOPE']).items()): # one at a time, otherwise order is random
(body, from_addr) = parse_email(msgid, data, server)
server.remove_flags(msgid, [SEEN]) # unset seen flag, will stay bold in email client
if from_addr in shortcuts_inverted: # reverse lookup of a shortcut
from_addr = shortcuts_inverted[from_addr]
reply = "-" + from_addr + " * " + body # asterisk indicates a resend
send_message(fromcall, reply)
msgexists = True
messages.sort(reverse=True)
del messages[int(count):] # only the latest "count" messages
for message in messages:
for msgid, data in list(server.fetch(message, ['ENVELOPE']).items()): # one at a time, otherwise order is random
(body, from_addr) = parse_email(msgid, data, server)
server.remove_flags(msgid, [SEEN]) # unset seen flag, will stay bold in email client
if from_addr in shortcuts_inverted: # reverse lookup of a shortcut
from_addr = shortcuts_inverted[from_addr]
reply = "-" + from_addr + " * " + body # asterisk indicates a resend
send_message(fromcall, reply)
msgexists = True
if msgexists is not True:
stm = time.localtime()
h = stm.tm_hour
m = stm.tm_min
s = stm.tm_sec
# append time as a kind of serial number to prevent FT1XDR from thinking this is a duplicate message.
# The FT1XDR pretty much ignores the aprs message number in this regard. The FTM400 gets it right.
reply = "No new msg " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
send_message(fromcall, reply)
if msgexists is not True:
stm = time.localtime()
h = stm.tm_hour
m = stm.tm_min
s = stm.tm_sec
# append time as a kind of serial number to prevent FT1XDR from thinking this is a duplicate message.
# The FT1XDR pretty much ignores the aprs message number in this regard. The FTM400 gets it right.
reply = "No new msg " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
send_message(fromcall, reply)
server.logout()
server.logout()
### end resend_email()
def check_email_thread():
# print "Email thread disabled."
# return
# print "Email thread disabled."
# return
LOG.debug("Starting Email thread")
threading.Timer(55, check_email_thread).start() # how do we skip first run?
threading.Timer(55, check_email_thread).start() # how do we skip first run?
shortcuts = CONFIG['shortcuts']
shortcuts_inverted = dict([[v,k] for k,v in shortcuts.items()]) # swap key/value
global shortcuts
shortcuts_inverted = dict([[v,k] for k,v in shortcuts.items()]) # swap key/value
date = datetime.datetime.now()
month = date.strftime("%B")[:3] # Nov, Mar, Apr
day = date.day
year = date.year
today = str(day) + "-" + month + "-" + str(year)
date = datetime.datetime.now()
month = date.strftime("%B")[:3] # Nov, Mar, Apr
day = date.day
year = date.year
today = str(day) + "-" + month + "-" + str(year)
LOG.debug("Connect to IMAP host '%s' with user '%s'" %
(CONFIG['imap']['host'],
CONFIG['imap']['login']))
server = IMAPClient('imap.yourdomain.com', use_uid=True)
server.login('KM6XXX@yourdomain.org', 'yourpassword')
select_info = server.select_folder('INBOX')
server = IMAPClient(CONFIG['imap']['host'], use_uid=True)
server.login(CONFIG['imap']['login'], CONFIG['imap']['password'])
# select_info = server.select_folder('INBOX')
messages = server.search(['SINCE', today])
#print("%d messages received today" % len(messages))
messages = server.search(['SINCE', today])
LOG.debug("%d messages received today" % len(messages))
for msgid, data in server.fetch(messages, ['ENVELOPE']).items():
envelope = data[b'ENVELOPE']
#print('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date ))
f = re.search('([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)', str(envelope.from_[0]) )
if f is not None:
from_addr = f.group(1)
else:
from_addr = "noaddr"
for msgid, data in server.fetch(messages, ['ENVELOPE']).items():
envelope = data[b'ENVELOPE']
LOG.debug('ID:%d "%s" (%s)' %
(msgid, envelope.subject.decode(), envelope.date))
f = re.search('([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)',
str(envelope.from_[0]) )
if f is not None:
from_addr = f.group(1)
else:
from_addr = "noaddr"
if "APRS" not in server.get_flags(msgid)[msgid]: #if msg not flagged as sent via aprs
m = server.fetch([msgid], ['RFC822'])
(body, from_addr) = parse_email(msgid, data, server)
server.remove_flags(msgid, [SEEN]) # unset seen flag, will stay bold in email client
if "APRS" not in server.get_flags(msgid)[msgid]: #if msg not flagged as sent via aprs
m = server.fetch([msgid], ['RFC822'])
(body, from_addr) = parse_email(msgid, data, server)
server.remove_flags(msgid, [SEEN]) # unset seen flag, will stay bold in email client
if from_addr in shortcuts_inverted: # reverse lookup of a shortcut
from_addr = shortcuts_inverted[from_addr]
if from_addr in shortcuts_inverted: # reverse lookup of a shortcut
from_addr = shortcuts_inverted[from_addr]
reply = "-" + from_addr + " " + body
#print "Sending message via aprs: " + reply
send_message(BASECALLSIGN, reply) #radio
server.add_flags(msgid, ['APRS']) #flag message as sent via aprs
server.remove_flags(msgid, [SEEN]) #unset seen flag, will stay bold in email client
reply = "-" + from_addr + " " + body
#print "Sending message via aprs: " + reply
send_message(CONFIG['ham']['callsign'], reply) #radio
server.add_flags(msgid, ['APRS']) #flag message as sent via aprs
server.remove_flags(msgid, [SEEN]) #unset seen flag, will stay bold in email client
server.logout()
server.logout()
### end check_email()
def send_ack_thread(tocall, ack, retry_count):
tocall = tocall.ljust(9) # pad to nine chars
line = USER + ">APRS::" + tocall + ":ack" + str(ack) + "\n"
line = CONFIG['aprs']['login'] + ">APRS::" + tocall + ":ack" + str(ack) + "\n"
for i in range(retry_count, 0, -1):
print "Sending ack __________________ Tx(" + str(i) + ")"
print "Raw : " + line,
print "To : " + tocall
print "Ack number : " + str(ack)
LOG.info("Sending ack __________________ Tx(" + str(i) + ")")
LOG.info("Raw : " + line)
LOG.info("To : " + tocall)
LOG.info("Ack number : " + str(ack))
tn.write(line)
time.sleep(31) # aprs duplicate detection is 30 secs? (21 only sends first, 28 skips middle)
return()
@ -289,15 +273,17 @@ def send_ack(tocall, ack):
def send_message_thread(tocall, message, this_message_number, retry_count):
global ack_dict
line = USER + ">APRS::" + tocall + ":" + message + "{" + str(this_message_number) + "\n"
line = (CONFIG['aprs']['login'] + ">APRS::" + tocall + ":" + message +
"{" + str(this_message_number) + "\n")
for i in range(retry_count, 0, -1):
print "DEBUG: send_message_thread msg:ack combos are: "
pprint.pprint(ack_dict)
LOG.debug("DEBUG: send_message_thread msg:ack combos are: ")
LOG.debug(pprint.pformat(ack_dict))
if ack_dict[this_message_number] != 1:
print "Sending message_______________ " + str(this_message_number) + "(Tx" + str(i) + ")"
print "Raw : " + line,
print "To : " + tocall
print "Message : " + message
LOG.info("Sending message_______________ " +
str(this_message_number) + "(Tx" + str(i) + ")")
LOG.info("Raw : " + line)
LOG.info("To : " + tocall)
LOG.info("Message : " + message)
tn.write(line)
sleeptime = (retry_count - i + 1) * 31 # decaying repeats, 31 to 93 second intervals
time.sleep(sleeptime)
@ -308,242 +294,318 @@ def send_message_thread(tocall, message, this_message_number, retry_count):
def send_message(tocall, message):
global message_number
global ack_dict
retry_count = 3
if message_number > 98: # global
message_number = 0
message_number += 1
if len(ack_dict) > 90: # empty ack dict if it's really big, could result in key error later
print "DEBUG: Length of ack dictionary is big at " + str(len(ack_dict)) + " clearing."
ack_dict.clear()
pprint.pprint(ack_dict)
print "DEBUG: Cleared ack dictionary, ack_dict length is now " + str(len(ack_dict)) + "."
ack_dict[message_number] = 0 # clear ack for this message number
tocall = tocall.ljust(9) # pad to nine chars
message = message[:67] # max? ftm400 displays 64, raw msg shows 74
# and ftm400-send is max 64. setting this to
# 67 displays 64 on the ftm400. (+3 {01 suffix)
# feature req: break long ones into two msgs
thread = threading.Thread(target = send_message_thread, args = (tocall, message, message_number, retry_count))
thread.start()
return()
global message_number
global ack_dict
retry_count = 3
if message_number > 98: # global
message_number = 0
message_number += 1
if len(ack_dict) > 90: # empty ack dict if it's really big, could result in key error later
LOG.debug("DEBUG: Length of ack dictionary is big at " + str(len(ack_dict)) + " clearing.")
ack_dict.clear()
LOG.debug(pprint.pformat(ack_dict))
LOG.debug("DEBUG: Cleared ack dictionary, ack_dict length is now " + str(len(ack_dict)) + ".")
ack_dict[message_number] = 0 # clear ack for this message number
tocall = tocall.ljust(9) # pad to nine chars
message = message[:67] # max? ftm400 displays 64, raw msg shows 74
# and ftm400-send is max 64. setting this to
# 67 displays 64 on the ftm400. (+3 {01 suffix)
# feature req: break long ones into two msgs
thread = threading.Thread(
target = send_message_thread,
args = (tocall, message, message_number, retry_count))
thread.start()
return()
### end send_message()
def process_message(line):
f = re.search('^(.*)>', line)
fromcall = f.group(1)
searchstring = '::' + USER + '[ ]*:(.*)' # verify this, callsign is padded out with spaces to colon
m = re.search(searchstring, line)
fullmessage = m.group(1)
f = re.search('^(.*)>', line)
fromcall = f.group(1)
searchstring = '::' + CONFIG['aprs']['login'] + '[ ]*:(.*)' # verify this, callsign is padded out with spaces to colon
m = re.search(searchstring, line)
fullmessage = m.group(1)
ack_attached = re.search('(.*){([0-9A-Z]+)', fullmessage) # ack formats include: {1, {AB}, {12
if ack_attached: # "{##" suffix means radio wants an ack back
message = ack_attached.group(1) # message content
ack_num = ack_attached.group(2) # suffix number to use in ack
else:
message = fullmessage
ack_num = "0" # ack not requested, but lets send one as 0
ack_attached = re.search('(.*){([0-9A-Z]+)', fullmessage) # ack formats include: {1, {AB}, {12
if ack_attached: # "{##" suffix means radio wants an ack back
message = ack_attached.group(1) # message content
ack_num = ack_attached.group(2) # suffix number to use in ack
else:
message = fullmessage
ack_num = "0" # ack not requested, but lets send one as 0
print "Received message______________"
print "Raw : " + line
print "From : " + fromcall
print "Message : " + message
print "Msg number : " + str(ack_num)
LOG.info("Received message______________")
LOG.info("Raw : " + line)
LOG.info("From : " + fromcall)
LOG.info("Message : " + message)
LOG.info("Msg number : " + str(ack_num))
return (fromcall, message, ack_num)
return (fromcall, message, ack_num)
### end process_message()
def send_email(to_addr, content):
print "Sending Email_________________"
global shortcuts
if to_addr in shortcuts:
print "To : " + to_addr ,
to_addr = shortcuts[to_addr]
print " (" + to_addr + ")"
subject = BASECALLSIGN
#content = content + "\n\n(NOTE: reply with one line)"
print "Subject : " + subject
print "Body : " + content
LOG.info("Sending Email_________________")
shortcuts = CONFIG['shortcuts']
if to_addr in shortcuts:
LOG.info("To : " + to_addr)
to_addr = shortcuts[to_addr]
LOG.info(" (" + to_addr + ")")
subject = CONFIG['ham']['callsign']
# content = content + "\n\n(NOTE: reply with one line)"
LOG.info("Subject : " + subject)
LOG.info("Body : " + content)
msg = MIMEText(content)
msg['Subject'] = subject
msg['From'] = "KM6XXX@yourdomain.org"
msg['To'] = to_addr
s = smtplib.SMTP_SSL('smtp.yourdomain.com', 465)
s.login("KM6XXX@yourdomain.org", "yourpassword")
try:
s.sendmail("KM6XXX@yourdomain.org", [to_addr], msg.as_string())
except Exception, e:
print "Sendmail Error!!!!!!!!!"
s.quit()
return(-1)
s.quit()
return(0)
msg = MIMEText(content)
msg['Subject'] = subject
msg['From'] = "KM6XXX@yourdomain.org"
msg['To'] = to_addr
s = smtplib.SMTP_SSL('smtp.yourdomain.com', 465)
s.login("KM6XXX@yourdomain.org", "yourpassword")
try:
s.sendmail("KM6XXX@yourdomain.org", [to_addr], msg.as_string())
except Exception:
LOG.exception("Sendmail Error!!!!!!!!!")
s.quit()
return(-1)
s.quit()
return(0)
### end send_email
# Setup the logging faciility
# to disable logging to stdout, but still log to file
# use the --quiet option on the cmdln
def setup_logging(args):
global LOG
levels = {
'CRITICAL': logging.CRITICAL,
'ERROR': logging.ERROR,
'WARNING': logging.WARNING,
'INFO': logging.INFO,
'DEBUG': logging.DEBUG}
log_level = levels[args.loglevel]
LOG.setLevel(log_level)
log_format = ("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]"
" %(message)s")
date_format = '%m/%d/%Y %I:%M:%S %p'
log_formatter = logging.Formatter(fmt=log_format,
datefmt=date_format)
fh = RotatingFileHandler('aprsd.log',
maxBytes=(10248576*5),
backupCount=4)
fh.setFormatter(log_formatter)
LOG.addHandler(fh)
if not args.quiet:
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(log_formatter)
LOG.addHandler(sh)
# This method tries to parse the config yaml file
# and consume the settings.
# If the required params don't exist,
# it will look in the environment
def parse_config(args):
# for now we still use globals....ugh
global CONFIG, LOG
def fail(msg):
LOG.critical(msg)
sys.exit(-1)
def check_option(config, section, name=None):
if section in config:
if name and name not in config[section]:
fail("'%s' was not in '%s' section of config file" %
(name, section))
else:
fail("'%s' section wasn't in config file" % section)
# Now read the ~/.aprds/config.yml
config = utils.get_config()
check_option(config, 'shortcuts')
check_option(config, 'ham', 'callsign')
check_option(config, 'aprs', 'login')
check_option(config, 'aprs', 'password')
check_option(config, 'aprs', 'host')
check_option(config, 'imap', 'host')
check_option(config, 'imap', 'login')
check_option(config, 'imap', 'password')
CONFIG = config
LOG.info("aprsd config loaded")
### main() ###
def main():
def main(args=args):
setup_logging(args)
time.sleep(2)
LOG.info("APRSD Started")
parse_config(args)
LOG.debug("Signal handler setup")
signal.signal(signal.SIGINT, signal_handler)
tn.write("user " + USER + " pass " + PASS + " vers aprsd 0.99\n" )
time.sleep(2)
setup_connection()
time.sleep(2)
user = CONFIG['aprs']['login']
password = CONFIG['aprs']['password']
LOG.info("LOGIN to APRSD with user '%s'" % user)
tn.write("user " + user + " pass " + password + " vers aprsd 0.99\n" )
time.sleep(2)
check_email_thread() # start email reader thread
check_email_thread() # start email reader thread
while True:
line = ""
try:
for char in tn.read_until("\n",100):
line = line + char
line = line.replace('\n', '')
print line
searchstring = '::' + USER
if re.search(searchstring, line): # is aprs message to us, not beacon, status, etc
(fromcall, message, ack) = process_message(line)
else:
message = "noise"
continue
while True:
line = ""
try:
for char in tn.read_until("\n",100):
line = line + char
line = line.replace('\n', '')
LOG.info(line)
searchstring = '::' + user
# is aprs message to us, not beacon, status, etc
if re.search(searchstring, line):
(fromcall, message, ack) = process_message(line)
else:
message = "noise"
continue
# ACK (ack##)
if re.search('^ack[0-9]+', message):
a = re.search('^ack([0-9]+)', message) # put message_number:1 in dict to record the ack
ack_dict.update({int(a.group(1)):1})
continue
# ACK (ack##)
if re.search('^ack[0-9]+', message):
# put message_number:1 in dict to record the ack
a = re.search('^ack([0-9]+)', message)
ack_dict.update({int(a.group(1)):1})
continue
# EMAIL (-)
elif re.search('^-.*', message): # is email command
searchstring = '^' + BASECALLSIGN + '.*'
if re.search(searchstring, fromcall): # only I can do email
r = re.search('^-([0-9])[0-9]*$', message) # digits only, first one is number of emails to resend
if r is not None:
resend_email(r.group(1))
elif re.search('^-([A-Za-z0-9_\-\.@]+) (.*)', message): # -user@address.com body of email
a = re.search('^-([A-Za-z0-9_\-\.@]+) (.*)', message) # (same search again)
if a is not None:
to_addr = a.group(1)
content = a.group(2)
if content == 'mapme': # send recipient link to aprs.fi map
content = "Click for my location: http://aprs.fi/" + BASECALLSIGN
too_soon = 0
now = time.time()
if ack in email_sent_dict: # see if we sent this msg number recently
timedelta = now - email_sent_dict[ack]
if ( timedelta < 300 ): # five minutes
too_soon = 1
if not too_soon or ack == 0:
send_result = send_email(to_addr, content)
if send_result != 0:
send_message(fromcall, "-" + to_addr + " failed")
else:
#send_message(fromcall, "-" + to_addr + " sent")
if len(email_sent_dict) > 98: # clear email sent dictionary if somehow goes over 100
print "DEBUG: email_sent_dict is big (" + str(len(email_sent_dict)) + ") clearing out."
email_sent_dict.clear()
email_sent_dict[ack] = now
# EMAIL (-)
elif re.search('^-.*', message): # is email command
searchstring = '^' + CONFIG['ham']['callsign'] + '.*'
if re.search(searchstring, fromcall): # only I can do email
r = re.search('^-([0-9])[0-9]*$', message) # digits only, first one is number of emails to resend
if r is not None:
resend_email(r.group(1))
elif re.search('^-([A-Za-z0-9_\-\.@]+) (.*)', message): # -user@address.com body of email
a = re.search('^-([A-Za-z0-9_\-\.@]+) (.*)', message) # (same search again)
if a is not None:
to_addr = a.group(1)
content = a.group(2)
if content == 'mapme': # send recipient link to aprs.fi map
content = "Click for my location: http://aprs.fi/" + CONFIG['ham']['callsign']
too_soon = 0
now = time.time()
if ack in email_sent_dict: # see if we sent this msg number recently
timedelta = now - email_sent_dict[ack]
if ( timedelta < 300 ): # five minutes
too_soon = 1
if not too_soon or ack == 0:
send_result = send_email(to_addr, content)
if send_result != 0:
send_message(fromcall, "-" + to_addr + " failed")
else:
#send_message(fromcall, "-" + to_addr + " sent")
if len(email_sent_dict) > 98: # clear email sent dictionary if somehow goes over 100
LOG.debug("DEBUG: email_sent_dict is big (" + str(len(email_sent_dict)) + ") clearing out.")
email_sent_dict.clear()
email_sent_dict[ack] = now
else:
LOG.info("Email for message number " + ack + " recently sent, not sending again.")
else:
print "\nEmail for message number " + ack + " recently sent, not sending again.\n"
else:
send_message(fromcall, "Bad email address")
send_message(fromcall, "Bad email address")
# TIME (t)
elif re.search('^t', message):
stm = time.localtime()
h = stm.tm_hour
m = stm.tm_min
cur_time = fuzzy(h, m, 1)
reply = cur_time + " (" + str(h) + ":" + str(m).rjust(2, '0') + "PDT)" + " (" + message.rstrip() + ")"
thread = threading.Thread(target = send_message, args = (fromcall, reply))
thread.start()
# TIME (t)
elif re.search('^t', message):
stm = time.localtime()
h = stm.tm_hour
m = stm.tm_min
cur_time = fuzzy(h, m, 1)
reply = cur_time + " (" + str(h) + ":" + str(m).rjust(2, '0') + "PDT)" + " (" + message.rstrip() + ")"
thread = threading.Thread(target = send_message, args = (fromcall, reply))
thread.start()
# FORTUNE (f)
elif re.search('^f', message):
process = subprocess.Popen(['/usr/games/fortune', '-s', '-n 60'], stdout=subprocess.PIPE)
reply = process.communicate()[0]
send_message(fromcall, reply.rstrip())
# FORTUNE (f)
elif re.search('^f', message):
process = subprocess.Popen(['/usr/games/fortune', '-s', '-n 60'], stdout=subprocess.PIPE)
reply = process.communicate()[0]
send_message(fromcall, reply.rstrip())
# PING (p)
elif re.search('^p', message):
stm = time.localtime()
h = stm.tm_hour
m = stm.tm_min
s = stm.tm_sec
reply = "Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
send_message(fromcall, reply.rstrip())
# PING (p)
elif re.search('^p', message):
stm = time.localtime()
h = stm.tm_hour
m = stm.tm_min
s = stm.tm_sec
reply = "Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
send_message(fromcall, reply.rstrip())
# LOCATION (l) "8 Miles E Auburn CA 1771' 38.91547,-120.99500 0.1h ago"
elif re.search('^l', message):
# get my last location, get descriptive name from weather service
try:
url = "http://api.aprs.fi/api/get?name=" + fromcall + "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
response = urllib.urlopen(url)
aprs_data = json.loads(response.read())
lat = aprs_data['entries'][0]['lat']
lon = aprs_data['entries'][0]['lng']
try: # altitude not always provided
alt = aprs_data['entries'][0]['altitude']
except:
alt = 0
altfeet = int(alt * 3.28084)
aprs_lasttime_seconds = aprs_data['entries'][0]['lasttime']
aprs_lasttime_seconds = aprs_lasttime_seconds.encode('ascii',errors='ignore') #unicode to ascii
delta_seconds = time.time() - int(aprs_lasttime_seconds)
delta_hours = delta_seconds / 60 / 60
url2 = "https://forecast.weather.gov/MapClick.php?lat=" + str(lat) + "&lon=" + str(lon) + "&FcstType=json"
response2 = urllib.urlopen(url2)
wx_data = json.loads(response2.read())
reply = wx_data['location']['areaDescription'] + " " + str(altfeet) + "' " + str(lat) + "," + str(lon) + " " + str("%.1f" % round(delta_hours,1)) + "h ago"
reply = reply.encode('ascii',errors='ignore') # unicode to ascii
send_message(fromcall, reply.rstrip())
except:
reply = "Unable to find you (send beacon?)"
send_message(fromcall, reply.rstrip())
# LOCATION (l) "8 Miles E Auburn CA 1771' 38.91547,-120.99500 0.1h ago"
elif re.search('^l', message):
# get my last location, get descriptive name from weather service
try:
url = "http://api.aprs.fi/api/get?name=" + fromcall + "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
response = urllib.urlopen(url)
aprs_data = json.loads(response.read())
lat = aprs_data['entries'][0]['lat']
lon = aprs_data['entries'][0]['lng']
try: # altitude not always provided
alt = aprs_data['entries'][0]['altitude']
except:
alt = 0
altfeet = int(alt * 3.28084)
aprs_lasttime_seconds = aprs_data['entries'][0]['lasttime']
aprs_lasttime_seconds = aprs_lasttime_seconds.encode('ascii',errors='ignore') #unicode to ascii
delta_seconds = time.time() - int(aprs_lasttime_seconds)
delta_hours = delta_seconds / 60 / 60
url2 = "https://forecast.weather.gov/MapClick.php?lat=" + str(lat) + "&lon=" + str(lon) + "&FcstType=json"
response2 = urllib.urlopen(url2)
wx_data = json.loads(response2.read())
reply = wx_data['location']['areaDescription'] + " " + str(altfeet) + "' " + str(lat) + "," + str(lon) + " " + str("%.1f" % round(delta_hours,1)) + "h ago"
reply = reply.encode('ascii',errors='ignore') # unicode to ascii
send_message(fromcall, reply.rstrip())
except:
reply = "Unable to find you (send beacon?)"
send_message(fromcall, reply.rstrip())
# WEATHER (w) "42F(68F/48F) Haze. Tonight, Haze then Chance Rain."
elif re.search('^w', message):
# get my last location from aprsis then get weather from weather service
try:
url = "http://api.aprs.fi/api/get?name=" + fromcall + "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
response = urllib.urlopen(url)
aprs_data = json.loads(response.read())
lat = aprs_data['entries'][0]['lat']
lon = aprs_data['entries'][0]['lng']
url2 = "https://forecast.weather.gov/MapClick.php?lat=" + str(lat) + "&lon=" + str(lon) + "&FcstType=json"
response2 = urllib.urlopen(url2)
wx_data = json.loads(response2.read())
reply = wx_data['currentobservation']['Temp'] + "F(" + wx_data['data']['temperature'][0] + "F/" + wx_data['data']['temperature'][1] + "F) " + wx_data['data']['weather'][0] + ". " + wx_data['time']['startPeriodName'][1] + ", " + wx_data['data']['weather'][1] + "."
reply = reply.encode('ascii',errors='ignore') # unicode to ascii
send_message(fromcall, reply.rstrip())
except:
reply = "Unable to find you (send beacon?)"
send_message(fromcall, reply)
# WEATHER (w) "42F(68F/48F) Haze. Tonight, Haze then Chance Rain."
elif re.search('^w', message):
# get my last location from aprsis then get weather from weather service
try:
url = "http://api.aprs.fi/api/get?name=" + fromcall + "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
response = urllib.urlopen(url)
aprs_data = json.loads(response.read())
lat = aprs_data['entries'][0]['lat']
lon = aprs_data['entries'][0]['lng']
url2 = "https://forecast.weather.gov/MapClick.php?lat=" + str(lat) + "&lon=" + str(lon) + "&FcstType=json"
response2 = urllib.urlopen(url2)
wx_data = json.loads(response2.read())
reply = wx_data['currentobservation']['Temp'] + "F(" + wx_data['data']['temperature'][0] + "F/" + wx_data['data']['temperature'][1] + "F) " + wx_data['data']['weather'][0] + ". " + wx_data['time']['startPeriodName'][1] + ", " + wx_data['data']['weather'][1] + "."
reply = reply.encode('ascii',errors='ignore') # unicode to ascii
send_message(fromcall, reply.rstrip())
except:
reply = "Unable to find you (send beacon?)"
send_message(fromcall, reply)
# USAGE
else:
reply = "usage: time, fortune, loc, weath, -emailaddr emailbody, -#(resend)"
send_message(fromcall, reply)
# USAGE
else:
reply = "usage: time, fortune, loc, weath, -emailaddr emailbody, -#(resend)"
send_message(fromcall, reply)
time.sleep(1) # let any threads do their thing, then ack
send_ack(fromcall, ack) # send an ack last
time.sleep(1) # let any threads do their thing, then ack
send_ack(fromcall, ack) # send an ack last
except Exception, e:
print "Error in mainline loop:"
print "%s" % str(e)
print "Exiting."
#sys.exit(1) # merely a suggestion
os._exit(1)
except Exception, e:
LOG.error("Error in mainline loop:")
LOG.error("%s" % str(e))
LOG.error("Exiting.")
#sys.exit(1) # merely a suggestion
os._exit(1)
# end while True
tn.close()
exit()
# end while True
tn.close()
exit()
if __name__ == "__main__":
main()
main(args)

View File

@ -1,12 +1,20 @@
"""Utilities and helper functions."""
import logging
import os
import pprint
import sys
import yaml
# an example of what should be in the ~/.aprsd/config.yml
example_config = '''
ham:
callsign: KFART
aprs:
login: someusername
password: password
host: noam.aprs2.net
shortcuts:
'aa': '5551239999@vtext.com'
'cl': 'craiglamparter@somedomain.org'
@ -19,12 +27,10 @@ smtp:
imap:
login: imapuser
password: something dumb
ham:
callsign: something
basename: somebasename
'''
log = logging.getLogger('APRSD')
def env(*vars, **kwargs):
"""This returns the first environment variable set.
if none are non-empty, defaults to '' or keyword arg default
@ -44,6 +50,6 @@ def get_config():
config = yaml.load(stream)
return config
else:
print("%s is missing, please create a config file" % config_file)
print("example config is\n %s" % example_config)
log.critical("%s is missing, please create a config file" % config_file)
print("\nCopy to ~/.aprsd/config.yml and edit\n\nSample config:\n %s" % example_config)
sys.exit(-1)