#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # # Example: python weatherboy.py -l 22664159 -u c -d 30 -a import gtk import gobject import time import webbrowser from decimal import Decimal from urllib2 import urlopen, URLError from argparse import ArgumentParser from xml.dom import minidom parser = ArgumentParser(description='Simple weather applet', epilog='Free software under GPL license.' 'Please, report bugs and comments on https://github.com/decayofmind/weatherboy') parser.add_argument('-l', '--location', required=True, metavar='WOEID', help='location WOEID (more on http://developer.yahoo.com/weather/)') parser.add_argument('-u', '--units', choices=['c', 'f'], default='c', metavar='c|f', help='units to display') parser.add_argument('-d', '--delta', default='10', type=int, metavar='N', help='timeout in minutes between next weather data query') parser.add_argument('-a', '--advanced', action='store_true', default=False, help='Advanced tooltip') class Api: def __init__(self, location, units): self.params = (location, units) self.url = 'http://xml.weather.yahoo.com/forecastrss?w=%s&u=%s' self.namespace = 'http://xml.weather.yahoo.com/ns/rss/1.0' self.website = 'http://weather.yahoo.com/' self.codes = { # code:icon name '0': 'weather-severe-alert', '1': 'weather-severe-alert', '2': 'weather-severe-alert', '3': 'weather-severe-alert', '4': 'weather-storm', '5': 'weather-snow-rain', '6': 'weather-snow-rain', '7': 'weather-snow', '8': 'weather-freezing-rain', '9': 'weather-fog', '10': 'weather-freezing-rain', '11': 'weather-showers', '12': 'weather-showers', '13': 'weather-snow', '14': 'weather-snow', '15': 'weather-snow', '16': 'weather-snow', '17': 'weather-snow', '18': 'weather-snow', '19': 'weather-fog', '20': 'weather-fog', '21': 'weather-fog', '22': 'weather-fog', '23': 'weather-few-clouds', '24': 'weather-few-clouds', '25': 'weather-few-clouds', '26': 'weather-overcast', '27': 'weather-clouds-night', '28': 'weather-clouds', '29': 'weather-few-clouds-night', '30': 'weather-few-clouds', '31': 'weather-clear-night', '32': 'weather-clear', '33': 'weather-clear-night', '34': 'weather-clear', '35': 'weather-snow-rain', '36': 'weather-clear', '37': 'weather-storm', '38': 'weather-storm', '39': 'weather-storm', '40': 'weather-showers-scattered', '41': 'weather-snow', '42': 'weather-snow', '43': 'weather-snow', '44': 'weather-few-clouds', '45': 'weather-storm', '46': 'weather-snow', '47': 'weather-storm', '3200': 'stock-unknown' } @staticmethod def conv_direction(value): value = Decimal(value) if 0 <= value < 22.5: return u'\u2193 (N)' elif 22.5 <= value < 67.5: return u'\u2199 (NE)' elif 67.5 <= value < 112.5: return u'\u2190 (E)' elif 112.5 <= value < 157.5: return u'\u2196 (SE)' elif 157.5 <= value < 202.5: return u'\u2191 (S)' elif 202.5 <= value < 247.5: return u'\u2197 (SW)' elif 247.5 <= value < 292.5: return u'\u2192 (W)' elif 292.5 <= value < 337.5: return u'\u2198 (NW)' else: return u'\u2193 (N)' def get_data(self): try: url = self.url % self.params dom = minidom.parse(urlopen(url)) units_node = dom.getElementsByTagNameNS(self.namespace, 'units')[0] units = {'temperature': units_node.getAttribute('temperature'), 'distance': units_node.getAttribute('distance'), 'pressure': units_node.getAttribute('pressure'), 'speed': units_node.getAttribute('speed')} forecasts = [] for node in dom.getElementsByTagNameNS(self.namespace, 'forecast'): forecasts.append({ 'date': node.getAttribute('date'), 'low': u'{0}\u00B0 {1}'.format(node.getAttribute('low'), units['temperature']), 'high': u'{0}\u00B0 {1}'.format(node.getAttribute('high'), units['temperature']), 'condition': node.getAttribute('text'), 'icon': self.codes.get(node.getAttribute('code')) }) condition = dom.getElementsByTagNameNS(self.namespace, 'condition')[0] location = dom.getElementsByTagNameNS(self.namespace, 'location')[0] wind = dom.getElementsByTagNameNS(self.namespace, 'wind')[0] atmosphere = dom.getElementsByTagNameNS(self.namespace, 'atmosphere')[0] return { 'current_condition': condition.getAttribute('text'), 'current_icon': self.codes.get(condition.getAttribute('code')), 'current_temp': u'{0}\u00B0 {1}'.format(condition.getAttribute('temp'), units['temperature']), 'extra': { 'wind': { 'direction': self.conv_direction(wind.getAttribute('direction')), 'speed': '{0} {1}'.format(wind.getAttribute('speed'), units['speed']) }, 'atmosphere': { 'humidity': '{0}%'.format(atmosphere.getAttribute('humidity')), 'visibility': '{0} {1}'.format(atmosphere.getAttribute('visibility'), units['distance']), 'pressure': '{0} {1}'.format(atmosphere.getAttribute('pressure'), units['pressure']) } }, 'forecasts': forecasts, 'location': { 'city': location.getAttribute('city'), 'country': location.getAttribute('country') }, 'timeStamp': time.strftime("%Y-%m-%d %H:%M") } except URLError: return None class MainApp: def __init__(self, args): self.args = args self.weather = None self.tooltip = None self.tray = gtk.StatusIcon() self.tray.connect('popup-menu', self.on_right_click) self.tray.connect('activate', self.on_left_click) self.tray.set_has_tooltip(True) if self.args.advanced: self.tray.connect('query-tooltip', self.on_tooltip_advanced) self.api = Api(self.args.location, self.args.units) self.timer_id = -1 self.update_tray() self.set_timer() def on_tooltip_advanced(self, widget, x, y, keyboard_mode, tooltip): #if self.tooltip: #tooltip.set_text(self.tooltip) if self.weather: weather = self.weather tooltip_text = '{0}\n{1}'.format(self.weather['current_temp'], self.weather['current_condition']) vbox = gtk.VBox() header = gtk.Label() header.set_markup( '{0}, {1}'.format(self.weather['location']['city'], self.weather['location']['country'])) header.set_alignment(1.0, 0.5) footer = gtk.Label() footer.set_markup('Last checked: {0}'.format(self.weather['timeStamp'])) separator_h = gtk.HSeparator() hbox = gtk.HBox() now_image = gtk.Image() now_image.set_padding(0, 5) now_image.set_pixel_size(48) now_image.set_from_icon_name(weather['current_icon'], 48) now_label = gtk.Label() now_label.set_markup('{0}'.format(tooltip_text)) now_label.set_padding(5, 5) table = gtk.Table(columns=2, homogeneous=False) u = 0 l = 1 for k, v in self.weather['extra'].iteritems(): h_label = gtk.Label() h_label.set_markup('{0}'.format(k)) h_label.set_alignment(0.0, 0.5) h_label.set_padding(5, 0) table.attach(h_label, 0, 1, u, l) for i, j in v.iteritems(): u += 1 l += 1 k_label = gtk.Label(i) k_label.set_alignment(0.0, 0.5) v_label = gtk.Label(j) v_label.set_alignment(0.0, 0.5) table.attach(k_label, 0, 1, u, l) table.attach(v_label, 1, 2, u, l) u += 1 l += 1 hbox.pack_start(now_image, False, False, 0) hbox.pack_start(now_label, False, False, 0) vbox.pack_start(header, True, False, 0) vbox.pack_start(separator_h, False, False, 0) vbox.pack_start(hbox, False, False, 0) vbox.pack_start(table, False, False, 0) vbox.pack_start(footer, False, False, 0) vbox.show_all() tooltip.set_custom(vbox) else: tooltip.set_text('Connection error!') return True def on_refresh(self, widget): self.update_tray() self.set_timer() def on_right_click(self, icon, event_button, event_time): menu = gtk.Menu() refresh = gtk.MenuItem('Refresh') refresh.show() refresh.connect('activate', self.on_refresh) quit = gtk.MenuItem('Quit') quit.show() quit.connect('activate', gtk.main_quit) menu.append(refresh) menu.append(quit) menu.popup(None, None, gtk.status_icon_position_menu, event_button, event_time, self.tray) def on_left_click(self, widget): webbrowser.open(self.api.website) def set_timer(self): self.remove_timer() self.timer_id = gobject.timeout_add_seconds(self.args.delta * 60, self.update_tray) def remove_timer(self): if self.timer_id > 0: gobject.source_remove(self.timer_id) self.timer_id = -1 def update_tray(self): self.weather = self.api.get_data() if self.weather is not None: self.tray.set_from_icon_name(self.weather['current_icon']) if not self.args.advanced: tooltip_text = '{0} / {1}'.format(self.weather['current_temp'], self.weather['current_condition']) self.tray.set_tooltip_markup(tooltip_text) else: if not self.args.advanced: self.tray.set_tooltip_text('Connection error!') self.tray.set_from_stock('gtk-dialog-error') return True if __name__ == "__main__": try: args = parser.parse_args() MainApp(args) gtk.main() except KeyboardInterrupt: pass