awips2/cave/com.raytheon.viz.avnconfig/localization/aviation/python/AvnWatch.py
Max Schenkelberg 6f60751ec6 Issue #2033 moved avnfps and text workstation files into respective plugins.
Change-Id: If95cb839ad81ca2a842ff7f6926847ac3928d8f2

Former-commit-id: 77e1a4d8f5237e5fae930c1e00589c752f8b3738
2013-08-15 12:21:43 -05:00

1418 lines
52 KiB
Python

##
# 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('<Button-3>', 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('<Button-3>',
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()