mirror of
https://github.com/craigerl/aprsd.git
synced 2024-11-25 17:38:44 -05:00
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:
parent
4c8d9c3b2c
commit
ce7a30aa78
790
aprsd/main.py
790
aprsd/main.py
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user