## # This software was developed and / or modified by Raytheon Company, # pursuant to Contract DG133W-05-CQ-1067 with the US Government. # # U.S. EXPORT CONTROLLED TECHNICAL DATA # This software product contains export-restricted data whose # export/transfer/disclosure is restricted by U.S. law. Dissemination # to non-U.S. persons whether in the United States or abroad requires # an export license or other authorization. # # Contractor Name: Raytheon Company # Contractor Address: 6825 Pine Street, Suite 340 # Mail Stop B8 # Omaha, NE 68106 # 402.291.0100 # # See the AWIPS II Master Rights File ("Master Rights File.pdf") for # further licensing information. ## # # Name: # AvnWatch.py # GFS1-NHD:A3644.0000-SCRIPT;68 # # Status: # DELIVERED # # History: # Revision 68 (DELIVERED) # Created: 25-SEP-2008 09:40:49 OBERFIEL # Updated Help Dialog contents to be consistent with main # GUI. # # Revision 67 (DELIVERED) # Created: 18-APR-2008 14:18:14 OBERFIEL # Fixed Typos in the Help Dialog, update CCFP section # # Revision 66 (DELIVERED) # Created: 26-FEB-2008 14:25:36 OBERFIEL # Fixed _talk notification (unimplemented) and DTG for Nov # 2008 # # Revision 65 (REVIEW) # Created: 19-FEB-2008 13:26:07 OBERFIEL # Updated Globals.Colors list to reflect user's changes. # # Revision 64 (REVIEW) # Created: 18-JAN-2008 08:04:46 GILMOREDM # Changed to use forecaster defined colors for status window # # Revision 63 (DELIVERED) # Created: 21-SEP-2007 17:26:08 OBERFIEL # Reorganized alert messages for flight category alerts in # balloon popup. Changed wording and added additional # dynamic info flt cat msgs. # # Revision 62 (DELIVERED) # Created: 29-MAY-2007 12:19:41 OBERFIEL # Updated colophon. Fixed logic within MetarMonitorP.py to # produce same behavior as 3.4 for # for vis and cig monitoring. Fixed AvnWatch to better # detect flight category type messages. # # Revision 61 (DELIVERED) # Created: 25-MAY-2007 14:27:10 OBERFIEL # Update to support additional information in remarks # # Revision 60 (DELIVERED) # Created: 22-MAY-2007 10:11:42 GILMOREDM # Added impactPlacement which is configurable through the # options interface and code changes that allow the # configuration of placement of impact statements within # # Revision 59 (REVIEW) # Created: 18-MAY-2007 09:56:32 GILMOREDM # changed order of taf, metar, and messages; # stripped out trailing newline on metar text # # Revision 58 (UNDER WORK) # Created: 18-APR-2007 12:32:44 SOLSON # Removed the CR characters that were present in previous rev # of this item. # # Revision 57 (DELIVERED) # Created: 06-DEC-2006 14:10:41 BLI # Modified to make xmit configurable for each user # # Revision 56 (BUILD_RELEASE) # Created: 30-NOV-2006 09:34:01 OBERFIEL # Updated Help documentation. Thanks Amanda. # # Revision 55 (BUILD_RELEASE) # Created: 17-NOV-2006 11:02:59 BLI # Changed to call the new climate GUI # # Revision 54 (DELIVERED) # Created: 29-JUN-2006 06:36:14 OBERFIEL # Updated to remove references to HTML and other obsolete # stuff # # Revision 53 (DELIVERED) # Created: 20-MAY-2006 09:26:27 OBERFIEL # Updated documentation and removed references to HTML # documentation # # Revision 52 (DELIVERED) # Created: 24-MAR-2006 17:45:43 TROJAN # spr 7106 Pmw code does not forward python exceptions to # AvnFPS _Logger # # Revision 51 (DELIVERED) # Created: 24-MAR-2006 09:48:23 TROJAN # spr 7103: redirect all error messages to a log file # # Revision 50 (DELIVERED) # Created: 15-FEB-2006 14:34:42 TROJAN # fixes transient dialog behavior - spr 7091 # # Revision 49 (APPROVED) # Created: 27-JAN-2006 16:00:37 TROJAN # stdr 956 # # Revision 48 (DELIVERED) # Created: 16-AUG-2005 13:03:19 TROJAN # spr 6989 # # Revision 47 (DELIVERED) # Created: 08-AUG-2005 13:13:55 TROJAN # spr 6971 # # Revision 46 (DELIVERED) # Created: 07-JUL-2005 12:14:47 TROJAN # spr 6548, 6887 # # Revision 45 (DELIVERED) # Created: 07-MAY-2005 11:31:03 OBERFIEL # Added Item Header Block # # Revision 44 (DELIVERED) # Created: 02-APR-2005 17:02:16 TROJAN # spr 6763 # # Revision 43 (APPROVED) # Created: 21-MAR-2005 14:39:06 TROJAN # spr 6735 # # Revision 42 (DELIVERED) # Created: 11-MAR-2005 15:55:30 TROJAN # spr 6717 # # Revision 41 (DELIVERED) # Created: 14-FEB-2005 20:54:48 TROJAN # spr 6649 # # Revision 40 (APPROVED) # Created: 24-JAN-2005 21:18:47 TROJAN # spr 6612 # # Revision 39 (APPROVED) # Created: 24-JAN-2005 15:42:44 TROJAN # spr 6604 # # Revision 38 (APPROVED) # Created: 30-SEP-2004 20:53:42 TROJAN # stdr 873 # # Revision 37 (APPROVED) # Created: 19-AUG-2004 20:32:56 OBERFIEL # Code change # # Revision 36 (APPROVED) # Created: 12-JUL-2004 12:13:02 OBERFIEL # Modified startup interface # # Revision 35 (APPROVED) # Created: 09-JUL-2004 19:10:41 OBERFIEL # Replaced busy dialogs # # Revision 34 (APPROVED) # Created: 01-JUL-2004 14:59:13 OBERFIEL # Update # # Revision 33 (DELIVERED) # Created: 17-MAR-2004 19:40:09 TROJAN # sprs for 2.1 # # Revision 32 (DELIVERED) # Created: 09-JAN-2004 15:27:55 PCMS # Updating for code cleanup # # Revision 31 (REVIEW) # Created: 08-JAN-2004 21:39:52 PCMS # Updating for code cleanup # # Revision 30 (APPROVED) # Created: 03-DEC-2003 18:42:56 TROJAN # spr 5681 # # Revision 29 (APPROVED) # Created: 05-NOV-2003 19:05:45 OBERFIEL # Initial version for 2.0 # # Revision 28 (DELIVERED) # Created: 24-APR-2003 14:54:46 TROJAN # sprs 5055, 5056, 5057, 5070 # # Revision 27 (DELIVERED) # Created: 10-APR-2003 15:26:55 TROJAN # spr 4997 # # Revision 26 (BUILD_RELEASE) # Created: 10-MAR-2003 13:39:02 TROJAN # sprs 4904 - 4908 # # Revision 25 (BUILD_RELEASE) # Created: 28-FEB-2003 12:34:34 TROJAN # spr 4749 4753 4815 4823 # # Revision 24 (DELIVERED) # Created: 24-OCT-2002 16:37:53 PCMS # Fixing more changes related to new NWS rules (Tempo group) # # Revision 23 (DELIVERED) # Created: 21-OCT-2002 21:52:57 PCMS # Updating of rnew NWSI 10-813 migration # # Revision 22 (DELIVERED) # Created: 24-SEP-2002 22:20:58 PCMS # Separate editor windows are created when invoking the # forecast editor from TAFGen, TWBGen and Amd/Cor buttons in # the main GUI. # # Revision 21 (DELIVERED) # Created: 02-AUG-2002 14:11:27 PCMS # Implementing autoupdate of text windows with incoming # products # # Revision 20 (REVIEW) # Created: 24-JUL-2002 16:07:28 PCMS # Fixed problem using incorrect forecast group. # # Revision 19 (DELIVERED) # Created: 24-JUL-2002 15:17:25 PCMS # Fixed problem when incorrect forecast group is used. # # Revision 18 (DELIVERED) # Created: 24-JUL-2002 14:41:56 PCMS # Additional fixes for 5.2.1 patch # # Revision 17 (DELIVERED) # Created: 23-JUL-2002 14:24:25 PCMS # Modified to compare TAFs & METARs every 10 mins rather than # upon arrival. # # Revision 16 (DELIVERED) # Created: 17-JUL-2002 13:20:36 PCMS # Implemented functionality to compare METARs and TAFs every # 10 minutes rather than upon arrival. # # Revision 15 (DELIVERED) # Created: 16-JUL-2002 20:35:26 PCMS # Set comparison of TAF and METARS to be done every 10 # minutes # # Revision 14 (DELIVERED) # Created: 09-JUL-2002 21:07:05 PCMS # Fixed missing line break in error message and invalid path # when workstation uses automounter. # # Revision 13 (DELIVERED) # Created: 25-JUN-2002 19:48:34 PCMS # Updating documentation for AVN Watch # # Revision 12 (DELIVERED) # Created: 18-JUN-2002 19:26:49 PCMS # Fixed no display of METAR data on startup when last hour # obs missing. # # Revision 11 (BUILD_RELEASE) # Created: 14-JUN-2002 15:11:06 PCMS # Fixed time problems which affected which TAF is monitored. # # Revision 10 (DELIVERED) # Created: 29-MAY-2002 22:17:58 PCMS # Adding dialog to edit resource configuration file # # Revision 9 (DELIVERED) # Created: 13-MAY-2002 21:45:17 PCMS # Fixed viewing problem when TAFs with improper time on TEMPO # group are decoded. # # Revision 8 (DELIVERED) # Created: 03-DEC-2001 19:13:15 PCMS # SPR 2981 # # Revision 7 (DELIVERED) # Created: 14-NOV-2001 21:19:51 PCMS # Fixing when rules take affect # # Revision 6 (DELIVERED) # Created: 06-NOV-2001 23:13:08 PCMS # updating # # Revision 5 (DELIVERED) # Created: 06-NOV-2001 20:58:39 PCMS # New rules take effect immediately. # # Revision 4 (DELIVERED) # Created: 30-OCT-2001 18:17:05 PCMS # Made the ordering of the bulleting list configurable. # # Revision 3 (DELIVERED) # Created: 10-OCT-2001 20:06:28 PCMS # Allow to select multiple products in the editor window # # Revision 2 (DELIVERED) # Created: 02-OCT-2001 17:48:36 PCMS # Updating for gui changes # # Revision 1 (DELIVERED) # Created: 20-AUG-2001 20:29:43 MOELLER # Initial version # # Change Document History: # 1: # Change Document: GFS1-NHD_SPR_7383 # Action Date: 06-NOV-2008 15:25:22 # Relationship Type: In Response to # Status: TEST # Title: AvnFPS: Lack of customization in QC check # # import Queue, atexit, itertools, logging, os, signal, time from Tkinter import * import Pmw import Pyro.core, Pyro.naming from Pyro.errors import * import AppShell, Avn, AvnLib, AvnDialog, AvnParser, AvnThread import Busy, DataRequestServ, ErrorRedirect, Globals, TafDecoder from HelpDialog import HelpDialog from Balloon import Balloon Python = os.environ['PYTHON'] TopDir = os.environ['TOP_DIR'] _Help = { 'title': 'AvnFPS Help', 'content': """ This is the main interface to AvnFPS. All menus and buttons have associated help. When the mouse cursor is placed over context sensitive help's 'hot spot', a 'balloon' message will appear. Successful completion of a task is usually shown in a message window at the bottom of the main GUI or dialogs. Important system messages will be shown there also. When an error occurs, a warning/error dialog will pop up which requires acknowledgment before one can interact further with the application. Menu options: File: Check Now: Forces check of all TAFs and transmission status. Restart: Restarts the program, using current configuration Quit: Terminates the application. Options: Setup: Calls setup configuration dialog which allows for setting configuration resources: fonts, colors and values that affect program behavior. Alert: Used to select alert criteria 'on-the-fly' when the program detects a condition requiring forecaster's action. Blink: If selected, station id button will blink when new notification arrives Help: Used to provide version number and location of AvnFPS documentation web sites and this help window. TAF Editor: Starts TAF editor Climate: Displays Climate GUI Plot: Displays Weather Plot GUI Backup: Invokes list dialog allowing selection of products to monitor. Server status indicators. green means server is running, red indicates serious misconfiguration of a server(s) or a large (> 1 minute) clock difference (skew) between px2f and workstations. DATA-xxx: Provides data to the GUI. The monitor will not function without this server running. INGEST-xxx: Data ingest server. You may still issue forecasts when this server is not running, although program will not update with new information as it arrives. XMIT-xxx: Forecast transmission server Queue: Background color indicates whether last issued forecast was successfully transmitted. The button invokes transmission queue control dialog. Product monitoring window consists of the following units: Site Id button: used to invoke TAF editor. Its background color is used to indicate problem with data. A new alert will cause the button to blink. Press right mouse button to stop blinking. Last TAF and METAR time labels: those display issue time. When either one is late, the corresponding label is highlighted. If there is no TAF, or TAF is older than 24 hours, time is set to None for both TAF and MTR. For each monitored data source there is a set of labels indicating whether a particular weather element is in agreement with the forecast. The following data sources are currently available: Current Observation: Most recent observation Nhr Persistence: Most recent observation compared to forecast N hours ahead ltg: Real-time CG lightning strikes rltg: Radar-based 3 hour lightning probability forecast NDFD Grids: GFE generated grids llws: Low Level Wind Shear, based on METAR and radars', profilers' or aircrafts' vertical wind profile data ccfp: Collaborative Convective Forecast Product from AWC Depending on configuration, some of the above can be accessed through popup menus associated with the data source heading labels. Use right mouse button to display the menu. Not all labels have an associated menu. By pointing mouse cursor at a particular data source you will get the forecast, that data values and list of violated rules, if any, displayed in a balloon message. Optional shortcut buttons to the TAF Editor. Amd: call TAF editor initialized for amended TAF for selected site. Rtd: call TAF editor initialized for delayed TAF for selected site. Cor: call TAF editor initialized for corrected TAF for selected site. """ } _Logger = logging.getLogger(__name__) ############################################################################### def _exitfun(): pass ############################################################################### class BButton(Button): # blinking button def __init__(self, master, *args, **kw): if 'blinkvar' in kw: self._tkBlink = kw['blinkvar'] del kw['blinkvar'] else: self._tkBlink = IntVar() self._tkBlink.set(1) Button.__init__(self, master, *args, **kw) self.bg = self.cget('background') self.abg = self.cget('activebackground') self._timer = None self._num = 0 # binding to cancel blinking self.bind('', Avn.curry(self.stopblink, self.bg)) def setbackground(self, bg): self.bg = bg self.configure(background=self.bg) def blink(self, num=0): if self._num > 0 and self._tkBlink.get() == 0: self.stopblink() if num > 0: self._num = num self._on = 0 if self._timer is not None: self.after_cancel(self._timer) self._on = not self._on if self._on: self.configure(background=self.bg) else: self.configure(background=self.abg) if self._num > 0: self._num -= 1 self._timer = self.after(1000, self.blink) else: # finished with blinks self._timer = None self.configure(background=self.bg) self._on = 0 def stopblink(self, bg=None, event=None): if bg: self.setbackground(bg) self._num = 0 ############################################################################### class TrafficLight(Pmw.MegaWidget): def __init__(self, parent = None, **kw): optiondefs = ( ('ok', 'green', Pmw.INITOPT), ('warning', 'yellow', Pmw.INITOPT), ('error', 'red', Pmw.INITOPT), ('size', 24, Pmw.INITOPT), ('labelmargin', 0, Pmw.INITOPT), ('labelpos', 'w', Pmw.INITOPT), ('sticky', 'news', Pmw.INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Pmw.MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() self._canvas = self.createcomponent('canvas', (), None, Canvas, (interior,), relief='raised', width=self['size'], height=self['size'] ) self._canvas.grid(column=2, row=2, sticky=self['sticky']) interior.grid_columnconfigure(2, weight=1) interior.grid_rowconfigure(2, weight=1) orig0, orig1 = 2, self['size']-2 self._light = self._canvas.create_oval(orig0, orig0, orig1, orig1) self.createlabel(interior) # Check keywords and initialise options. self.initialiseoptions() def get(self): color = self._canvas.itemcget(self._light, 'fill') for k in ['ok', 'warning', 'error']: if color == self[k]: return k return None def set(self, status): if status in ['ok', 'warning', 'error']: self._canvas.itemconfigure(self._light, fill=self[status]) else: self._canvas.itemconfigure(self._light, fill='grey') ############################################################################### class AlertDialog(AvnDialog.Dialog): # A pop-up dialog to configure alert levels AlertTypes = ['deiconify', 'raise', 'play'] Help = { 'deiconify': 'Deiconifies the applications', \ 'raise': 'Raises the application to the top', \ 'play': 'Plays sound file'} def __init__(self, parent): AvnDialog.Dialog.__init__(self, parent) Label(self.interior(), text='Alert Options').pack(side='top', pady=5, expand='yes', fill='x') itemlist = ['disabled'] + ['alertLevel%d' % (i+2) for i in xrange(len(Globals.Colors[2:]))] menulist = [] for alert in self.AlertTypes: menu = self.createcomponent(alert, (), None, Pmw.OptionMenu, (self.interior(),), labelpos='w', label_text=alert, menubutton_width=12, items=itemlist, command=self._setcolor ) menu.pack(side='top', padx=5) menulist.append(menu) Balloon().bind(menu.component('label'), self.Help[alert]) try: menu.invoke(self.option_get('notify%s' % alert.title(), '')) except ValueError: pass Pmw.alignlabels(menulist) def _setcolor(self,*args): pass def getitems(self): # Returns dictionary dict[alerttype] = color index d = {} for k in self.AlertTypes: value = self.component(k).getcurselection() if value == 'disabled': d[k] = 99 else: d[k] = int(value[-1]) return d ############################################################################### class SiteMonitor(object): def __init__(self, top, master, row, info): self.top = top # AvnWatch self.info = info self.errorColor = 'red' self.warningColor = 'orange' self.tempoColor = 'yellow' self.status = {} self.maxSeverity = 0 self.maxNewSeverity = 0 self._tkActive = IntVar() self._tkActive.set(1) self._taf = None self._bg = master.option_get('background', '') self.impactPlacement = master.option_get('impactPlacement','') self._monitors = [m['module'].Monitor(self.info, m) \ for m in top._activeMonitors] f = Frame(master, relief='ridge', borderwidth=2) f.grid(row=row, column=1, columnspan=3+len(self._monitors), padx=1, pady=1, sticky='news') kw = {'row': row, 'column': 1, 'padx': 2, 'pady': 4, 'sticky': 'news'} self.idbutton = BButton(master, width=5, text=self.info['ident'], background=self._bg, command=self.__showEditor, blinkvar=top._tkBlink, ) self.idbutton.grid(**kw); kw['column'] += 1 Balloon().bind(self.idbutton, 'Displays recent TAFs and METARs') self.checkbutton = Checkbutton(master, variable=self._tkActive, ) self.checkbutton.grid(**kw); kw['column'] += 1 Balloon().bind(self.checkbutton, 'If selected, this site is monitored') f = Frame(master) self.timelabel = {} for tag in ['TAF', 'MTR']: self.timelabel[tag] = Label(f, text='None', width=10, bd=0) self.timelabel[tag].pack(side='top', expand='no', pady=0) f.grid(**kw); kw['column'] += 1 self.label = {} for n, m in enumerate(self._monitors): f = Frame(master, relief='ridge', borderwidth=2) self.label[n] = {} for t in m.args['items']: l = Label(f, text=m.args['labels'][t][:3], background=Globals.Colors[1], width=3, ) l.pack(side='left') self.label[n][t] = l f.grid(**kw); kw['column'] += 1 self.status[n] = {} if int(master.option_get('amdbuttons', '0')): bbox = Pmw.ButtonBox(master, padx=0, pady=0) for tag in ['Amd', 'Rtd', 'Cor']: btn = bbox.add(tag, command=Avn.curry(self.__showEditor, Avn.tagToBBB(tag))) btn.configure(pady=1) Balloon().bind(btn, 'Calls TAF Editor') bbox.alignbuttons() kw['padx'], kw['pady'] = 0, 2 bbox.grid(**kw) def __bindTafReport(self, taf, newtaf): err = TafDecoder.errors(self._taf.dcd)['error'] msg = [] for e in err: msg.extend(e[1]['error']) try: # to deal with TAFs resent as delayed, depends on wording # in TafDecoder msg.remove('Issue and valid times do not match') except ValueError: pass if msg: if newtaf: self.idbutton.setbackground(self.warningColor) msg.append(taf) Balloon().bind(self.idbutton, '\n'.join(msg)) return self.idbutton.setbackground(self._bg) Balloon().bind(self.idbutton, 'Displays recent TAFs and METARs') def __configureWxLabels(self, tag, module, taf=''): """Make balloon messages for the indicators on main GUI""" def _cleanup_msg(text): """Flight category messages need to be treated a little differently""" fltcat = False numMETARs = text.count('METAR:') # # If its a flight category message: consolidate and simplify if numMETARs > 0: fltcat = True if numMETARs > 1: mtrtafinfo = '' fltcatmsgs = [] othermsgs = [] for lne in text.split('\n'): METARpos = lne.find('METAR:') if METARpos > 0: msg,mtrtafinfo = lne.split(':',1) fltcatmsgs.append(msg) elif METARpos == 0: mtrtafinfo = lne else: fltcatmsgs.append(lne.split(':',1)[0]) new_msg = '%s: %s' % (',\n'.join(fltcatmsgs), mtrtafinfo.strip()) return fltcat, new_msg return fltcat, text if not self.status[tag]: return text = self.status[tag].get('text', '') for item in module.args['items']: d = self.status[tag]['status'][item] fltcat, msg = _cleanup_msg(d.msg) label = self.label[tag][item] label.configure(background=Globals.Colors[d.severity]) if self.impactPlacement == 'top': Balloon().bind(label, '\n'.join([msg, taf, text]).rstrip()) elif self.impactPlacement == 'split': if fltcat: Balloon().bind(label, '\n'.join([taf, text.rstrip('\n'), msg]).rstrip()) else: Balloon().bind(label, '\n'.join([msg, taf, text]).rstrip()) elif self.impactPlacement == 'bottom': Balloon().bind(label, '\n'.join([taf, text.rstrip('\n'), msg]).rstrip()) else: Balloon().bind(label, '\n'.join([msg, taf, text]).rstrip()) def __configureTimeLabel(self, kind, t): if t == 0.0: # no data self.timelabel[kind].configure(text='%s None' % kind, background='grey') return elif time.time() > t + {'MTR': 3900, 'TAF': 24000}[kind]: color = self.warningColor else: color = self._bg self.timelabel[kind].configure(text='%s %s' % \ (kind, time.strftime('%H:%M', time.gmtime(t))), background=color) def __showEditor(self, type=None): self.idbutton.stopblink(self._bg) self.top.showTafEditor(self.info['ident'], type) def __getWarnLevels(self): # FIXME: currently alerts only for METARs tag = 0 m = self._monitors[tag] if not self.status[tag]: return [1]*len(m.args['items']) # missing data s = self.status[tag]['status'] return [s[i].severity for i in m.args['items']] def __setMissing(self, msg): for n, m in enumerate(self._monitors): self.status[n]['status'] = m.setMissing(msg) self.__configureWxLabels(n, m) self.__configureTimeLabel('TAF', 0.0) self.__configureTimeLabel('MTR', 0.0) self.idbutton.setbackground(self.errorColor) if self._taf: Balloon().bind(self.idbutton, '%s\n%s' % (msg, self._taf.text)) else: Balloon().bind(self.idbutton, msg) def checkStatus(self, event): if not self._tkActive.get(): return None result = {'ident': self.info['ident'], 'taf': self._taf, \ 'newtaf': False, 'status': {}} if event.src in ['ALL', 'tafs']: # new TAF or force check result['newtaf'] = True try: tafs = Globals.DRC.getTafs(self.info['ident'], True, time.time()-43200.0,1)[:1] if tafs: result['taf'] = tafs[0] if 'fatal' in result['taf'].dcd: raise Avn.AvnError('Cannot decode TAF') result['taf'].hourly = AvnLib.TafData( \ result['taf'].dcd['group']) else: raise Avn.AvnError('Cannot load TAF') for n, m in enumerate(self._monitors): result['status'][n] = m.compare(result['taf']) except Avn.AvnError, e: result['msg'] = str(e) else: for n, m in enumerate(self._monitors): if m.Source != event.src or result['taf'] is None \ or 'fatal' in result['taf'].dcd: continue result['status'][n] = m.compare(result['taf']) return result def updateGUI(self, result): self._taf = result.get('taf') if result['newtaf']: if 'msg' in result: self.__setMissing(result['msg']) # don't bother with the rest return self.idbutton.configure(background=self._bg) self.__configureTimeLabel('TAF', self._taf.dcd['itime']['value']) if not self._taf or 'fatal' in self._taf.dcd: return # save warning state previous = self.__getWarnLevels() self.status.update(result['status']) for n, m in enumerate(self._monitors): if not self.status[n]: continue s = self.status[n] taf = self._taf.text.rstrip() if n == 0: # METAR self.__bindTafReport(taf, result['newtaf']) if 'dcd' in s: t = s['dcd']['itime']['value'] else: t = 0.0 self.__configureTimeLabel('MTR', t) self.__configureWxLabels(n, m, taf) current = self.__getWarnLevels() self.maxSeverity = max(current) if result['newtaf'] or not previous: self.maxNewSeverity = self.maxSeverity else: # compare previous and current warning levels tmp = [x for (x, y) in zip(current, previous) if x > y] if tmp: self.maxNewSeverity = max(tmp) else: self.maxNewSeverity = 0 if self.maxSeverity <= 1: self.idbutton.stopblink() else: self.idbutton.blink(300) # blink for 5 min # update TAF Editor window if hasattr(self.top, 'tafEditor') and \ self.top.tafEditor.winfo_ismapped() and \ self.top.tafEditor.getSite() == self.info['ident']: self.top.tafEditor.updateViewer() def reset(self, monitors): # Sets internal data for a site to monitor self._monitors = [m['module'].Monitor(self.info, m) for m in monitors] color = Globals.Colors[1] for n, m in enumerate(self._monitors): self.status[n] = {} for tag in m.args['items']: self.label[n][tag].configure(background=color) Balloon().bind(self.label[n][tag], '') def getTaf(self): return self._taf def getMetar(self): return self.status[0] ############################################################################### class ServerLights(Frame): def __init__(self, master): Frame.__init__(self, master, relief=GROOVE, bd=2) self.light = {} def configure(self): for tag in Globals.ServerStatus: if tag not in self.light: self.light[tag] = TrafficLight(self, label_text=tag) self.light[tag].pack(side='left', padx=5) status = self.light[tag].get() newstatus = status if time.time() - Globals.ServerStatus[tag] < 60.0: newstatus = 'ok' else: newstatus = 'error' if status != newstatus: self.light[tag].set(newstatus) ############################################################################### class AvnWatch(AppShell.AppShell): appname = AppShell.AppShell.appname + ' Monitor' def __init__(self, **kw): self._guicfg = AvnParser.getGuiCfg() if not self._guicfg: raise SystemExit # # Up to seven alert colors are customizable in AvnFPS. Globals.Colors = [0,1,2,3,4,5,6] Globals.Viewers = self._guicfg['viewers'] Globals.EditTag = self._guicfg['edittags'] # import modules: replace module name by the module itself d = self._guicfg['monitors'] for k in d: d[k]['module'] = __import__(d[k]['module']) atexit.register(_exitfun) AppShell.AppShell.__init__(self, **kw) server = os.environ.get('PYRO_NS_HOSTNAME', 'local host') self.root.wm_title('%s connected to %s' % \ (self.root.wm_title(), server)) _Logger.info('Connected to %s, forecaster: %s', server, Globals.Forecaster) # redirect Pmw error messages ErrorRedirect.fixlogging(_Logger, self.interior()) # methods that override base class def appInit(self, **kw): self._tag = {'src': 'gui'} if not Globals.Products: Globals.Products = AvnParser.getTafProducts()[:1] if not Globals.Products: msg = 'TAFs not configured. Cannot continue' _Logger.exception(msg) Busy.showerror(msg, self.root) raise SystemExit # initialize variables self._status_timer = None self._sitemons = [] # list of current SiteMonitor widgets self._playFile = None self._checkNow = 1 self._lastPeriod = 0 # a half hour period of a forced check self._lastplay = 0.0 # time of sound alert self._tkBlink = IntVar() Globals.Forecaster = kw.get('forecaster', '') self.__getOptionDB() self.root.wm_iconbitmap(Avn.WatchBitmap) Globals.Colors = [str(self.option_get('alertLevel%d' % x, 'grey')) for x in xrange(7)] def createInterface(self): bbox = Pmw.ButtonBox(self.interior(), hull_relief='groove', hull_bd=2, padx=0, pady=0, ) if self._guicfg['features']['tafeditor']: btn = bbox.add('TAF Editor', command=self.showTafEditor) Balloon().bind(btn, 'TAF editor') if self._guicfg['features']['twbeditor']: btn = bbox.add('TWB Editor', command=self.__showTwbEditor) Balloon().bind(btn, 'TWB editor') btn = bbox.add('Climate', command=self.__showCondClimate) Balloon().bind(btn, 'Climatology Tools') btn = bbox.add('Plot', command=self.__showPlotDialog) Balloon().bind(btn, 'Graph') btn = bbox.add('Backup', command=self.__showProductSelectionDialog) Balloon().bind(btn, 'Product selection') bbox.alignbuttons() bbox.pack(side='top', expand='no', fill='x') # status lights f = Frame(self.interior()) self._lights = ServerLights(f) self._lights.pack(side='left', expand='no') self.statusbtn = BButton(f, background='green', text='Queue', command=self.__showQueue, ) Balloon().bind(self.statusbtn, 'Transmission log viewer') self.statusbtn.pack(side='right') f.pack(side='top', fill='x') self.frame = Pmw.ScrolledFrame(self.interior(), vscrollmode='static', ) interior = self.frame.interior() f = Frame(interior, relief='ridge', borderwidth=2) ncols = len(self._guicfg['menus']) wanteditbtns = int(self.option_get('amdbuttons', '0')) if wanteditbtns: ncols += 1 f.grid(row=0, column=1, columnspan=3+ncols, sticky='news') self.label = {} self._activeMonitors = [] for n, items in enumerate(self._guicfg['menus']): m = self._guicfg['monitors'][items[0]] self._activeMonitors.append(m) self.label[n] = Label(interior, text=m['menu']) self.label[n].grid(row=0, column=4+n, pady=3) if len(items) > 1: popup = Menu(f, tearoff=0, type='normal') for item in items: m = self._guicfg['monitors'][item] popup.add_command(label=m['menu'], command=Avn.curry(self.__reload, m, n)) self.label[n].bind('', Avn.curry(self.__popupMenu, popup)) if wanteditbtns: label = Label(interior, text='Editor Shortcuts') label.grid(row=0, column=5+n, pady=3) self.frame.pack(side='bottom', fill='both', expand='yes') # monitor method uses AlertDialog values self.__createAlertDialog() def __reload(self, monitor, n): self._activeMonitors[n] = monitor self.label[n].configure(text=monitor['menu']) for sm in self._sitemons: sm.reset(self._activeMonitors) self.__checkStatusNow() def __popupMenu(self, menu, e): menu.tk_popup(e.widget.winfo_rootx()+e.x, e.widget.winfo_rooty()+e.y) def createMenuBar(self): self.menubar.addmenuitem('Help', 'command', label='About...', command=self.showAbout, ) self.menubar.addmenuitem('Help', 'command', label='Usage ...', command=self.__showHelp, ) self.menubar.addmenuitem('File', 'command', label='Check Now', command=self.__checkStatusNow, ) self.menubar.addmenuitem('File', 'separator') self.menubar.addmenuitem('File', 'command', label='Restart', command=self.__restart, ) self.menubar.addmenuitem('File', 'separator') self.menubar.addmenuitem('File', 'command', label='Quit', command=self.exit, ) self.menubar.addmenu('Options', 'Configuration options') if Globals.Forecaster: self.menubar.addmenuitem('Options', 'command', label='Setup', command=self.__showResourceDialog ) self.menubar.addmenuitem('Options', 'command', label='Alert', command=self.__showAlertDialog, ) self.menubar.addmenuitem('Options', 'checkbutton', label='Blink', variable=self._tkBlink, ) def initializeTk(self): import XDefaults for item in XDefaults.Defaults: self.root.option_add(item[0], item[1], 30) # private methods # alert methods def __deiconify(self, ids, ix): self.root.deiconify() def __raise(self, ids, ix): self.root.tkraise() def __play(self, idis, ix): # avoid frequent bells now = time.time() if now < self._lastplay + 5.0: return self._lastplay = now cmd = Avn.playCommand(self._playFile) if cmd: os.system(cmd) def __talk(self, ids, ix): pass def __checkStatusNow(self): self._checkNow = 1 def __createAlertDialog(self): # Creates dialog for selection of alert levels self.alertdialog = AlertDialog(self.interior()) def __createQueueDialog(self): # Creates data ingest/transmission monitor dialog import QueueViewer self.queuedialog = QueueViewer.Viewer(self.interior()) self.queuedialog.setGeometry() def __createProductSelectionDialog(self): # Creates dialog for selection of products to monitor self.productdialog = Pmw.SelectionDialog(self.interior(), buttons=('OK', 'Close'), defaultbutton='OK', command=self.__setProductsToMonitor, scrolledlist_labelpos='n', label_text='Select product(s)', scrolledlist_listbox_selectmode='extended', scrolledlist_listbox_width=20, scrolledlist_listbox_height=4, scrolledlist_vscrollmode='static', ) self.productdialog.withdraw() def __getXResources(self): # Reads application resources file id = AvnParser.getForecasters().get(Globals.Forecaster, '')['id'] if id: path = os.path.join('etc', 'app-resources', 'X.'+str(id)) if os.path.isfile(path): self.option_readfile(path) return path = os.path.join('etc', 'app-resources', 'X') if os.path.isfile(path): self.option_readfile(path) else: msg = 'Cannot access default resources file,\n' + \ 'using build-in values' _Logger.info(msg) Busy.showwarning(msg, self.root) def __getOptionDB(self): # Sets application resources try: self.__getXResources() self._playFile = self.option_get('playFile', '') self._tkBlink.set(int(self.option_get('blink', ''))) Pmw.Color.setscheme(self.root, background=self.option_get('background', '')) except Exception: msg = 'Error processing X resources' _Logger.exception(msg) Busy.showerror(msg, self.root) def __listProducts(self): # Displays list of defined products sl = self.productdialog.component('scrolledlist') sl.setlist(AvnParser.getTafProducts()) # highlight currently selected products for index in xrange(sl.size()): if sl.get(index) in Globals.Products: sl.selection_set(index) def __restart(self): msg = 'Restarting' _Logger.info(msg) self.messagebar().message('userevent', msg) if self._status_timer: self.after_cancel(self._status_timer) self._eventClient.abort() if os.fork(): Globals.DRC.release() self.root.after(3000, self.quit) return logging.shutdown() os.execl(Python, 'avn'+os.path.basename(Python), os.path.join(TopDir, 'py', 'avnmenu.py'), '-f', Globals.Forecaster, *tuple(Globals.Products)) def __setProductsToMonitor(self, result): # Callback for product selection dialog if result != 'OK': self.productdialog.deactivate() return Globals.Products = self.productdialog.getcurselection() self.__setMainWindow() self.productdialog.deactivate() self.__checkStatusNow() def __showAlertDialog(self): self.alertdialog.setGeometry() self.alertdialog.display() def __showCondClimate(self): opts = '-f %s' %Globals.Forecaster os.spawnlp(os.P_NOWAIT,Python, Python, os.path.join(TopDir, 'py','avnclimate.py'), opts) def __showPlotDialog(self): if not hasattr(self, 'plotDialog'): import WxPlot self.plotDialog = WxPlot.WxPlot(self.interior()) self.plotDialog.setGeometry() self.plotDialog.setSite(self._sitemons) self.plotDialog.display() def __showTwbEditor(self): if not hasattr(self, 'twbEditor'): from TwbEditDialog import TwbEditor self.twbEditor = TwbEditor(self.interior()) self.twbEditor.display() def __showHelp(self): HelpDialog().display(_Help) def __showQueue(self): self.statusbtn.setbackground('green') # reset self.statusbtn.stopblink() if not hasattr(self, 'queuedialog'): self.__createQueueDialog() self.queuedialog.display() def __showProductSelectionDialog(self): if not hasattr(self, 'productdialog'): self.__createProductSelectionDialog() self.__listProducts() position = 'first+%d+%d' % (self.winfo_rootx()+30, \ self.winfo_rooty()+30) Busy.Manager.busy(None, self.productdialog.component('hull')) self.productdialog.activate(geometry=position) Busy.Manager.notbusy() # Globals.DRC.release() if hasattr(self, 'tafEditor'): self.tafEditor.setSite(self._sitemons) if hasattr(self, 'plotDialog'): self.plotDialog.setSite(self._sitemons) def __showResourceDialog(self): if not hasattr(self, 'resourceEditor'): from ResourceDialog import ResourceEditor self.resourceEditor = ResourceEditor(self.interior()) self.resourceEditor.setGeometry() self.resourceEditor.display() def __setMainWindow(self): # Initializes main display window for the selected sites # Unmap site widgets. row=0 are column headings for sm in self._sitemons: sm.idbutton.stopblink() interior = self.frame.interior() for w in interior.grid_slaves(): if w.grid_info()['row'] != '0': w.grid_forget() # create/configure site widgets tmp = [AvnParser.getTafProductCfg(p)['sites'] for p in Globals.Products] tafinfo = filter(None, [AvnParser.getTafSiteCfg(ident) \ for ident in itertools.chain(*tmp)]) self._sitemons = [SiteMonitor(self, interior, n+1, info) \ for (n, info) in enumerate(tafinfo)] self.update_idletasks() # resize window w = interior.winfo_reqwidth() + 5 h = min(interior.winfo_reqheight(), 512) self.frame.component('clipper').configure(width=w, height=h) def __setXmitStatus(self, msg): if not msg.value.startswith('FAIL') and \ not msg.value.startswith('SUCCESS'): return if msg.value.startswith('FAIL'): self.statusbtn.setbackground('red') self.messagebar().message('systemerror', msg.value) else: self.messagebar().message('systemevent', msg.value) self.statusbtn.blink(30) def __phonyMessage(self): # creates phony message that will make __checkData() work self._checkNow = 0 msg = Avn.Bunch(src='ALL', ident='ALL', file=None) return Avn.Bunch(time=time.time(), subject=Pyro.config.PYRO_NS_DEFAULTGROUP, msg=msg) def __alert(self, severity): if severity <= 0: return alertdict = self.alertdialog.getitems() for key, alertfun in [('deiconify', self.__deiconify), ('raise', self.__raise), ('play', self.__play)]: if severity >= alertdict[key]: alertfun(None, severity) def __startThreads(self): self._requestQueue = Queue.Queue() self._resultsQueue = Queue.Queue() self.__startEventClient() self._refresher = AvnThread.Worker(self._requestQueue, self._resultsQueue) def __startEventClient(self): self._eventClient = AvnThread.Subscriber(self._resultsQueue) self._eventCount = 0 def __listen(self): # Main monitoring method. currentPeriod = time.time()//1800.0 if self._lastPeriod != currentPeriod: self._lastPeriod = currentPeriod self._checkNow = 1 while True: if self._checkNow: # forced check i = AvnThread.Subscriber.requestId event = self.__phonyMessage() else: try: i, event = self._resultsQueue.get_nowait() except Queue.Empty: # re-subscribe every 30 seconds, in case Event Server # is restarted self._eventCount += 1 if self._eventCount >= 30: self._eventCount = 0 self._eventClient.subscribe() break try: if i == AvnThread.Subscriber.requestId: self.__processNotifications(event) else: self.__processMonitors(event) except Exception: _Logger.exception('Cannot process event') self._lights.configure() self._status_timer = self.after(1000, self.__listen) def __selectSiteMonitors(self, event): def _metars(sm): return event.msg.ident in sm.info['sites']['metar'] def _ident(sm): return event.msg.ident == sm.info['ident'] if event.msg.ident == 'ALL': return self._sitemons if event.msg.src == 'mtrs': return filter(_metars, self._sitemons) else: return filter(_ident, self._sitemons) def __processNotifications(self, event): if event.msg.src.startswith('INGEST') or \ event.msg.src.startswith('DATA'): Globals.ServerStatus[event.msg.src] = event.time elif event.msg.src.startswith('XMIT'): Globals.ServerStatus[event.msg.src] = event.time self.__setXmitStatus(event.msg) else: sms = self.__selectSiteMonitors(event) if not sms: return message = 'Checking %s for %s' % (event.msg.src, \ ' '.join([sm.info['ident'] for sm in sms])) _Logger.info(message) if event.msg.src == 'ALL': self.messagebar().message('userevent', 'Checking all data ...') else: self.messagebar().message('userevent', message) self.update_idletasks() for sm in sms: self._refresher.performWork(sm.checkStatus, event.msg) def __processMonitors(self, event): ident = event.get('ident', None) try: sm = [s for s in self._sitemons if s.info['ident']==ident][0] except IndexError: _Logger.exception('Invalid ident %s', ident) return sm.updateGUI(event) self.__alert(sm.maxNewSeverity) # public methods def run(self): # overrides Tkinter method self.__setMainWindow() # connect to data request server try: Globals.DRC = DataRequestServ.Client() self.__startThreads() self.__listen() except PyroError: msg = 'Cannot access servers' _Logger.error(msg) self.messagebar().message('systemerror', msg) # shut down forecaster selection menu os.kill(os.getppid(), signal.SIGUSR1) signal.signal(signal.SIGUSR1,signal.SIG_IGN) AppShell.AppShell.run(self) def showTafEditor(self, ident=None, bbb=''): if not hasattr(self, 'tafEditor'): from TafEditDialog import TafEditor self.tafEditor = TafEditor(self.interior()) self.tafEditor.setSite(self._sitemons, ident) if ident: if bbb: self.tafEditor.displayBulletin(bbb, 'Latest', ident) self.tafEditor.pager.selectpage('Editor') else: self.tafEditor.updateViewer() self.tafEditor.pager.selectpage('Viewer') else: self.tafEditor.pager.selectpage('Editor') self.tafEditor.display() def exit(self): if int(self.option_get('confirmClose', '')): if not Busy.askokcancel('Do you really want to quit?', self.root): return try: os.kill(os.getppid(), signal.SIGUSR1) except OSError: pass try: Globals.DRC.release() except: pass self.quit()