source: pvMail/src/PvMail/pvMail.py @ 1094

Last change on this file since 1094 was 1094, checked in by jemian, 10 years ago

v3.0.1

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Rev Url
File size: 15.2 KB
Line 
1#!/usr/bin/env python
2
3########### SVN repository information ###################
4# $Date: 2012-09-07 19:39:37 +0000 (Fri, 07 Sep 2012) $
5# $Author: jemian $
6# $Revision: 1094 $
7# $URL$
8# $Id: pvMail.py 1094 2012-09-07 19:39:37Z jemian $
9########### SVN repository information ###################
10
11'''
12===================================
13pvMail: combined CLI and GUI
14===================================
15
16Functionally based on pvMail UNIX shell script written in 1999.
17
18Summary
19--------
20
21Watches an EPICS PV and sends email when it changes from 0 to 1.
22PV value can be either integer or float.
23
24.. note::
25   When "running", wait for trigger PV to go from 0 to 1.  When that
26   happens, fetch mail message from message PV.  Then, send that
27   message out to each of the email addresses.  The message
28   content is prioritized for view on a small-screen device such
29   as a pager or a PDA or smartphone.
30
31:author: Kurt Goetze (original version)
32:author: Pete Jemian (this version)
33:organization: AES/BCDA, Advanced Photon Source, Argonne National Laboratory
34
35:license: Copyright (c) 2011, UChicago Argonne, LLC
36:license: Copyright (c) 2009, The University of Chicago, as Operator of Argonne
37     National Laboratory.
38:license: Copyright (c) 2009 The Regents of the University of California, as
39     Operator of Los Alamos National Laboratory.
40:license: This file is distributed subject to a Software License Agreement found
41     in the file LICENSE that is included with this distribution.
42
43:note: Version History:
44:note: 05.09.07  kag  Initial alpha version.  Needs testing.
45:note: 2009-12-02 prj: converted to use wxPython (no Tkinter or Pmw)
46:note: 2011-11-23 prj: complete rewrite using PyEpics
47    and combined GUI (Traits) and CLI
48   
49:requires: EPICS system (http://www.aps.anl.gov/epics)
50    with at least two process variables (PVs)
51    where the "Trigger PV" toggles between values of 0 and 1
52    and the "SendMessage PV" contains a string to send as part of
53    the email message.
54:requires: PyEpics (http://cars9.uchicago.edu/software/python/pyepics3/)
55:requires: Traits (http://code.enthought.com/projects/traits/)
56'''
57
58
59import argparse
60import datetime
61import epics
62import logging
63import os
64import socket
65import sys
66import threading
67import time
68import traceback
69
70
71__yyyymmdd__ = str(datetime.datetime.now()).split()[0]
72__project_name__ = "PvMail"
73__description__ = "Watch an EPICS PV. Send email when it changes from 0 to 1."
74__svnid__ = "$Id: pvMail.py 1094 2012-09-07 19:39:37Z jemian $"
75__version__ = "3"
76__minor_version__ = "0.1"
77__revision__ = __svnid__.split(" ")[2]
78#__full_version__ = "%s.%s-r%s" % (__version__, __minor_version__, __revision__)
79__full_version__ = "v%s.%s, %s" % (__version__, __minor_version__, __yyyymmdd__) 
80__author__ = "Pete Jemian"
81__institution__ = "Advanced Photon Source, Argonne National Laboratory"
82__author_email__= "jemian@anl.gov"
83__url__ = "http://subversion.xray.aps.anl.gov/admin_bcdaext/pvMail"
84__license__ = "(c) 2009-2012, UChicago Argonne, LLC"
85__license__ += " (see LICENSE file for details)"
86
87LOG_FILE = "pvMail-%d.log" % os.getpid()
88RETRY_INTERVAL_S = 0.2
89CHECKPOINT_INTERVAL_S = 5 * 60.0
90
91gui_object = None
92
93
94class PvMail(threading.Thread):
95    '''
96    Watch an EPICS PV (using PyEpics interface) and send an email
97    when the PV changes from 0 to 1.
98    '''
99   
100    def __init__(self):
101        self.trigger = False
102        self.message = "default message"
103        self.subject = "pvMail.py"
104        self.triggerPV = ""
105        self.messagePV = ""
106        self.recipients = []
107        self.old_value = None
108        self.monitoredPVs = []
109   
110    def basicChecks(self):
111        '''
112        check for valid inputs,
113        raise exceptions as discovered,
114        otherwise no return result
115        '''
116        if len(self.recipients) == 0:
117            msg = "need at least one email address for list of recipients"
118            raise RuntimeWarning, msg
119        fmt = "could not connect to %s PV: %s"
120        parts = {'message': self.messagePV, 
121                 'trigger': self.triggerPV}
122        for name, pv in parts.items():
123            if len(pv) == 0:
124                raise RuntimeWarning, "no name for the %s PV" % name
125            if pv not in self.monitoredPVs:
126                if self.testConnect(pv, timeout=0.5) is False:
127                    raise RuntimeWarning, fmt % (name, pv)
128       
129    def testConnect(self, pvname, timeout=5.0):
130        '''
131        create PV,
132        wait for connection,
133        return connection state (True | False)
134       
135        adapted from PyEpics __createPV() method
136        '''
137        logger("test connect with %s" % pvname)
138        retry_interval_s = 0.0001
139        start_time = time.time()
140        thispv = epics.PV(pvname)
141        thispv.connect()
142        while not thispv.connected:
143            time.sleep(retry_interval_s)
144            epics.ca.poll()
145            if time.time()-start_time > timeout:
146                break
147        return thispv.connected
148   
149    def do_start(self):
150        '''start watching for triggers'''
151        logger("do_start")
152        if len(self.monitoredPVs) == 0:
153            self.basicChecks()
154            logger("passed basicChecks(), starting monitors")
155            epics.camonitor(self.messagePV, callback=self.receiveMessageMonitor)
156            epics.camonitor(self.triggerPV, callback=self.receiveTriggerMonitor)
157            self.old_value = epics.caget(self.triggerPV)
158            # What happens if either self.messagePV or self.triggerPV
159            # are changed after self.do_start() is called?
160            # Keep a local list of what was initiated.
161            self.monitoredPVs.append(self.messagePV)
162            self.monitoredPVs.append(self.triggerPV)
163   
164    def do_stop(self):
165        '''stop watching for triggers'''
166        logger("do_stop")
167        for pv in self.monitoredPVs:
168            epics.camonitor_clear(pv)   # no problem if pv was not monitored
169        self.monitoredPVs = []
170   
171    def do_restart(self):
172        '''restart watching for triggers'''
173        self.do_stop()
174        self.do_start()
175   
176    def receiveMessageMonitor(self, value, **kw):
177        '''respond to EPICS CA monitors on message PV'''
178        logger("%s = %s" % (self.messagePV, value))
179        self.message = value
180   
181    def receiveTriggerMonitor(self, value, **kw):
182        '''respond to EPICS CA monitors on trigger PV'''
183        logger("%s = %s" % (self.triggerPV, value))
184        # print self.old_value, type(self.old_value), value, type(value)
185        if self.old_value == 0:
186            if value == 1:
187                # Cannot use this definition:
188                #     self.trigger = (value == 1)
189                # since the trigger PV just may transition back
190                # to zero before SendMessage() runs.
191                self.trigger = True
192                SendMessage(self)
193        self.old_value = value
194   
195    def send_test_message(self):
196        '''
197        sends a test message, used for development only
198        '''
199        logger("send_test_message")
200        self.recipients = ["jemian", "prjemian"]
201        message = ''
202        message += 'host: %s\n' % socket.gethostname()
203        message += 'date: %s\n' % datetime.datetime.now()
204        message += 'program: %s\n' % sys.argv[0]
205        message += 'trigger PV: %s\n' % self.triggerPV
206        message += 'message PV: %s\n' % self.messagePV
207        message += 'recipients: %s\n' % ", ".join(self.recipients)
208        self.subject = "pvMail development test"
209        sendMail(self.subject, self.message, self.recipients)
210
211
212class SendMessage(threading.Thread):
213    '''
214    initiate sending the message in a separate thread
215   
216    :param obj pvm: instance of PvMail object on which to report
217    '''
218
219    def __init__(self, pvm):
220        logger("SendMessage")
221        pvm.trigger = False        # triggered event received
222
223        try:
224            pvm.basicChecks()
225           
226            pvm.subject = "pvMail.py: " + pvm.triggerPV
227           
228            msg = pvm.message
229            msg += "\n\n"
230            msg += 'user: %s\n' % os.environ['LOGNAME']
231            msg += 'host: %s\n' % socket.gethostname()
232            msg += 'date: %s\n' % datetime.datetime.now()
233            msg += 'program: %s\n' % sys.argv[0]
234            msg += 'PID: %d\n' % os.getpid()
235            msg += 'trigger PV: %s\n' % pvm.triggerPV
236            msg += 'message PV: %s\n' % pvm.messagePV
237            msg += 'recipients: %s\n' % ", ".join(pvm.recipients)
238            pvm.message = msg
239
240            sendMail(pvm.subject, pvm.message, pvm.recipients)
241            logger("message(s) sent")
242        except:
243            err_msg = traceback.format_exc()
244            final_msg = "pvm.subject = %s\nmsg = %s\ntraceback: %s" % (pvm.subject, str(msg), err_msg)
245            logger(final_msg)
246
247def sendMail(subject, message, recipients):
248    '''
249    send an email message using sendmail
250   
251    :param str subject: short text for email subject
252    :param str message: full text of email body
253    :param [str] recipients: list of email addresses to receive the message
254    '''
255    global gui_object
256   
257    email_program = '/usr/lib/sendmail'
258    from_addr = sys.argv[0]
259    to_addr = str(" ".join(recipients))
260    mailprogram = "%s -F %s -t %s" % (email_program, from_addr, to_addr)
261    mail_command = [mailprogram, "Subject: "+subject, message]
262    cmd = '''cat << +++ | %s\n+++''' % "\n".join(mail_command)
263
264    msg = "sending email to: %s" % to_addr
265    logger(msg)
266    if gui_object is not None:
267        gui_object.SetStatus(msg)   # FIXME: get the GUI status line to update
268
269    try:
270        logger( "email command:\n" + cmd )
271        if os.path.exists(email_program):
272            os.popen(cmd)    # send the message
273        else:
274            logger( 'email program (%s) does not exist' % email_program )
275    except:
276        err_msg = traceback.format_exc()
277        final_msg = "cmd = %s\ntraceback: %s" % (cmd, err_msg)
278        logger(final_msg)
279
280
281def logger(message):
282    '''
283    log a report from this class.
284
285    :param str message: words to be logged
286    '''
287    now = datetime.datetime.now()
288    name = sys.argv[0]
289    name = os.path.basename(name)
290    text = "(%s,%s) %s" % (name, now, message)
291    logging.info(text)
292
293
294def basicStartTest():
295    '''simple test of the PvMail class'''
296    logging.basicConfig(filename=LOG_FILE,level=logging.INFO)
297    logger("startup")
298    pvm = PvMail()
299    pvm.recipients = ['prjemian@gmail.com']
300    pvm.triggerPV = "pvMail:trigger"
301    pvm.messagePV = "pvMail:message"
302    retry_interval_s = 0.05
303    end_time = time.time() + 60
304    report_time = time.time() + 5.0
305    pvm.do_start()
306    while time.time() < end_time:
307        if time.time() > report_time:
308            report_time = time.time() + 5.0
309            logger("time remaining: %.1f seconds ..." % (end_time - time.time()))
310        time.sleep(retry_interval_s)
311    pvm.do_stop()
312
313
314def basicMailTest():
315    '''simple test sending mail using the PvMail class'''
316    pvm = PvMail()
317    pvm.send_test_message()
318
319
320def cli(results):
321    '''
322    command-line interface to the PvMail class
323   
324    :param obj results: default parameters from argparse, see main()
325    '''
326    logging_interval = min(60*60, max(5.0, results.logging_interval))
327    sleep_duration = min(5.0, max(0.0001, results.sleep_duration))
328
329    pvm = PvMail()
330    pvm.triggerPV = results.trigger_PV
331    pvm.messagePV = results.message_PV
332    pvm.recipients = results.email_addresses.strip().split(",")
333    checkpoint_time = time.time()
334    pvm.do_start()
335    while True:     # endless loop, kill with ^C or equal
336        if time.time() > checkpoint_time:
337            checkpoint_time += logging_interval
338            logger("checkpoint")
339        if pvm.trigger:
340            SendMessage(pvm)
341        time.sleep(sleep_duration)
342    pvm.do_stop()        # this will never be called
343
344
345def gui(results):
346    '''
347    graphical user interface to the PvMail class
348   
349    :param obj results: default parameters from argparse, see main()
350    '''
351   
352    import traits_gui
353    global gui_object
354   
355    gui_object = traits_gui.PvMail_GUI(results.trigger_PV, 
356                                results.message_PV, 
357                                results.email_addresses.strip().split(","),
358                                results.log_file,
359                                )
360    gui_object.configure_traits()
361
362
363def main():
364    '''parse command-line arguments and choose which interface to use'''
365    parser = argparse.ArgumentParser(description=__description__)
366
367    # positional arguments
368    # not required if GUI option is selected
369    parser.add_argument('trigger_PV', action='store', nargs='?',
370                        help="EPICS trigger PV name", default="")
371
372    parser.add_argument('message_PV', action='store', nargs='?',
373                        help="EPICS message PV name", default="")
374
375    parser.add_argument('email_addresses', action='store', nargs='?',
376                        help="email address(es), comma-separated if more than one", 
377                        default="")
378
379    # optional arguments
380    parser.add_argument('-l', action='store', dest='log_file',
381                        help="for logging program progress and comments", 
382                        default=LOG_FILE)
383
384    parser.add_argument('-i', action='store', dest='logging_interval', 
385                        type=float,
386                        help="checkpoint reporting interval (s) in log file", 
387                        default=CHECKPOINT_INTERVAL_S)
388
389    parser.add_argument('-r', action='store', dest='sleep_duration', 
390                        type=float,
391                        help="sleep duration (s) in main event loop", 
392                        default=RETRY_INTERVAL_S)
393
394    parser.add_argument('-g', '--gui', action='store_true', default=False,
395                        dest='interface',
396                        help='Use the graphical rather than command-line interface')
397
398    parser.add_argument('-v', '--version', action='version', version=__full_version__)
399
400    results = parser.parse_args()
401
402    addresses = results.email_addresses.strip().split(",")
403    interface = {False: 'command-line', True: 'GUI'}[results.interface]
404
405    logging.basicConfig(filename=results.log_file, level=logging.INFO)
406    logger("#"*60)
407    logger("startup")
408    logger("trigger PV       = " + results.trigger_PV)
409    logger("message PV       = " + results.message_PV)
410    logger("email list       = " + str(addresses) )
411    logger("log file         = " + results.log_file)
412    logger("logging interval = " + str( results.logging_interval ) )
413    logger("sleep duration   = " + str( results.sleep_duration ) )
414    logger("interface        = " + interface)
415    logger("user             = " + os.environ['LOGNAME'])
416    logger("host             = " + socket.gethostname() )
417    logger("program          = " + sys.argv[0] )
418    logger("PID              = " + str(os.getpid()) )
419   
420    if results.interface is False:
421        # When the GUI is not selected,
422        # ensure the positional arguments are given
423        tests = [
424                     len(results.trigger_PV),
425                     len(results.message_PV),
426                     len(" ".join(addresses)),
427                ]
428        if 0 in tests:
429            parser.print_usage()
430            sys.exit()
431   
432    # call the interface
433    {False: cli, True: gui}[results.interface](results)     
434
435
436if __name__ == '__main__':
437    #  ./pvMail.py  pvMail:trigger pvMail:message jemian
438    if False:  # code development only
439        sys.argv.append('pvMail:trigger')
440        sys.argv.append('pvMail:message')
441        sys.argv.append('jemian')
442        sys.argv.append('-l mylog.log')
443    main()
Note: See TracBrowser for help on using the repository browser.