~conky-companions/+junk/conkyforecast

66 by m-buck
* Updated Dutch (nl) translation based on update from Mark Coumans
1
#!/usr/bin/env python2
2
# -*- coding: utf-8 -*-
3
###############################################################################
4
# conkyForecast.py is a (not so) simple (anymore) python script to gather 
5
# details of the current weather for use in conky.
6
#
7
#  Author: Kaivalagi
8
# Created: 13/04/2008
9
10
from datetime import datetime, timedelta, tzinfo
11
from optparse import OptionParser
12
import sys
13
import os
14
import socket
15
import urllib2
16
import gettext
17
import locale
18
import re
19
import codecs
20
import traceback
21
import time
22
import json
23
24
# not sure on these, might create more trouble, but here goes...
25
reload(sys)
26
sys.setdefaultencoding('utf-8')
27
28
# cPickle is a pickle class implemented in C - so its faster
29
# in case its not available, use regular pickle
30
try:
31
    import cPickle as pickle
32
except ImportError:
33
    import pickle
34
35
app_name = "conkyForecast"
36
app_path = os.path.dirname(os.path.abspath(__file__))
37
module_name = __file__.replace(os.path.dirname (__file__) + "/", "").replace(".pyc","").replace(".py", "")
38
39
# default to standard locale translation
40
domain = __file__.replace(os.path.dirname (__file__) + "/", "").replace(".py", "")
41
localedirectory = os.path.dirname(os.path.abspath(__file__)) + "/locale"
42
gettext.bindtextdomain(domain, localedirectory)
43
gettext.textdomain(domain)
44
gettext.install(domain)
45
46
class CommandLineParser:
47
48
    parser = None
49
50
    def __init__(self):
51
52
        self.parser = OptionParser()
53
        self.parser.add_option("-C", "--config", dest="config", default="~/.conkyForecastWU.config", type="string", metavar="FILE", help=u"[default: %default] The path to the configuration file, allowing multiple config files to be used.")
54
        self.parser.add_option("-l", "--location", dest="location", type="string", metavar="CODE", help=u"location code for weather data [default set in config]. Normally in the form \"COUNTRY/CITY\"")
55
        self.parser.add_option("-d", "--datatype", dest="datatype", default="HT", type="string", metavar="DATATYPE", help=u"[default: %default] The data type options are: DW (Day of Week), WI (Weather Icon Path), LT (Forecast:Low Temp,Current:Feels Like Temp), HT (Forecast:High Temp,Current:Current Temp), CT (Conditions Text), PC (Precipitation Chance), HM (Humidity), VI (Visibility), WD (Wind Direction), WA (Wind Angle - in degrees), WS (Wind Speed), WG (Wind Gusts), BF (Bearing Font), BI (Bearing Icon Path), BS (Bearing font with Speed), CN (City Name), CO (Country), OB (Observatory), BR (Barometer Reading), BD (Barometer Description), UI (UV Index), UT (UV Text), DP (Dew Point), LU (Last Update at weather.com), LF (Last Fetch from weather.com). Not applicable at command line when using templates.")
56
        self.parser.add_option("-s", "--startday", dest="startday", type="int", metavar="NUMBER", help=u"define the starting day number, if omitted current conditions are output. Not applicable at command line when using templates.")
57
        self.parser.add_option("-e", "--endday", dest="endday", type="int", metavar="NUMBER", help=u"define the ending day number, if omitted only starting day data is output. Not applicable at command line when using templates.")
58
        self.parser.add_option("-S", "--spaces", dest="spaces", type="int", default=1, metavar="NUMBER", help=u"[default: %default] Define the number of spaces between ranged output. Not applicable at command line when using templates.")
59
        self.parser.add_option("-t", "--template", dest="template", type="string", metavar="FILE", help=u"define a template file to generate output in one call. A displayable item in the file is in the form [--datatype=HT --startday=1]. The following are possible options within each item: --location,--datatype,--startday,--endday,--night,--shortweekday,--imperial,--beaufort,--metrespersecond,--hideunits,--hidedegreesymbol,--spaces,--minuteshide. Note that the short forms of the options are not supported! If any of these options is set from the commandline, it sets the default value of the option for all template items.")
60
        self.parser.add_option("-L", "--locale", dest="locale", type="string", help=u"override the system locale for language output (bg=bulgarian, cs=czech, de=german, es=spanish, en=english, es=spanish, fj=fijian, fr=french, it=italian, nl=dutch, pl=polish, ro=romanian, sk=slovak, more to come)")
61
        self.parser.add_option("-i", "--imperial", dest="imperial", default=False, action="store_true", help=u"request imperial units, if omitted output is in metric.")
62
        self.parser.add_option("-b", "--beaufort", dest="beaufort", default=False, action="store_true", help=u"request beaufort scale for wind speeds, if omitted output is either metric/imperial.")
63
        self.parser.add_option("-M", "--metrespersecond", dest="metrespersecond", default=False, action="store_true", help=u"request metres per second for wind speeds, if omitted output is either metric/imperial.")
64
        self.parser.add_option("-n", "--night", dest="night", default=False, action="store_true", help=u"switch output to night data, if omitted day output will be output.")
65
        self.parser.add_option("-w", "--shortweekday", dest="shortweekday", default=False, action="store_true", help=u"Shorten the day of week data type to 3 characters.")
66
        self.parser.add_option("-u", "--hideunits", dest="hideunits", default=False, action="store_true", help=u"Hide units such as mph or C, degree symbols (°) are still shown.")
67
        self.parser.add_option("-x", "--hidedegreesymbol", dest="hidedegreesymbol", default=False, action="store_true", help=u"Hide the degree symbol used with temperature output, this is only valid if used in conjunction with --hideunits.")
68
        self.parser.add_option("-m", "--minuteshide", dest="minuteshide", type="int", metavar="NUMBER", help=u"Works only with LU and LF. If present, hides the date part of the LU or LF timestamp if the day of the timestamp is today. The time part is also hidden, if the timestamp is older than minutes specified in this argument. If set to 0, the time part is always shown. If set to -1, the value EXPIRY_MINUTES from the config file is used.")
69
        self.parser.add_option("-c", "--centeredwidth", dest="centeredwidth", type="int", metavar="WIDTH", help=u"If used the output will be centered in a string of the set width, padded out with spaces, if the output width is greater than the setting it will be truncated")
70
        self.parser.add_option("-r", "--refetch", dest="refetch", default=False, action="store_true", help=u"Fetch data regardless of data expiry.")
71
        self.parser.add_option("-v", "--verbose", dest="verbose", default=False, action="store_true", help=u"Request verbose output, not a good idea when running through conky!")
72
        self.parser.add_option("-V", "--version", dest="version", default=False, action="store_true", help=u"Displays the version of the script.")
73
        self.parser.add_option("--errorlogfile", dest="errorlogfile", type="string", metavar="FILE", help=u"If a filepath is set, the script appends errors to the filepath.")
74
        self.parser.add_option("--infologfile", dest="infologfile", type="string", metavar="FILE", help=u"If a filepath is set, the script appends info to the filepath.")                
75
76
    def parse_args(self):
77
        (options, args) = self.parser.parse_args()
78
        return (options, args)
79
80
    def print_help(self):
81
        return self.parser.print_help()
82
    
83
# N.B: The below class member values are defaults and should be left alone, they
84
#      are there to provide a working script if the script is called without all
85
#      the expected input. Any issues raised where these values have been
86
#      changed will get the simple "put the .py file back to it's original
87
#      state" reply
88
class ForecastConfig:
89
    CACHE_FOLDERPATH = "/tmp/"
90
    CONNECTION_TIMEOUT = 5
91
    EXPIRY_MINUTES = 30
92
    TIME_FORMAT = "%H:%M"
93
    DATE_FORMAT = "%Y-%m-%d"
94
    LOCALE = "" # with no setting the default locale of the system is used
95
    XOAP_PARTNER_ID = "" # need config with correct partner id
96
    XOAP_LICENCE_KEY = "" # need config"N/A" with correct licence key
97
    DEFAULT_LOCATION = "UK/Norwich"
98
    MAXIMUM_DAYS_FORECAST = 7
99
    AUTO_NIGHT = False
100
    BASE_WU_JSON_URL = "http://api.wunderground.com/api/<WU_LICENCE_KEY>/geolookup/conditions/forecast/q/<LOCATION>.json"
101
    PROXY_HOST = None
102
    PROXY_PORT = 8080
103
    PROXY_USERNAME = None
104
    PROXY_PASSWORD = None
105
106
107
class ForecastDataset:
108
    def __init__(self, last_update, day_of_week, low, high, condition_url, condition_text, precip, humidity, wind_dir, wind_dir_numeric, wind_speed, wind_gusts, timezone, bar_read, bar_desc, uv_index, uv_text, dew_point, observatory, visibility, city, country):
109
        self.last_update = last_update
110
        self.day_of_week = day_of_week
111
        self.low = low
112
        self.high = high
113
        self.condition_url = condition_url
114
        self.condition_text = condition_text
115
        self.precip = precip
116
        self.humidity = humidity
117
        self.wind_dir = wind_dir
118
        self.wind_dir_numeric = wind_dir_numeric
119
        self.wind_speed = wind_speed
120
        self.wind_gusts = wind_gusts
121
        self.timezone = timezone
122
        self.bar_read = bar_read
123
        self.bar_desc = bar_desc
124
        self.uv_index = uv_index
125
        self.uv_text = uv_text
126
        self.dew_point = dew_point
127
        self.observatory = observatory
128
        self.visibility = visibility
129
        self.city = city
130
        self.country = country
131
        
132
class ForecastLocation:
133
    timestamp = None
134
    
135
    def __init__(self, current, day, night):
136
        self.current = current
137
        self.day = day
138
        self.night = night
139
        self.timestamp = datetime.today()
140
        
141
    def outdated(self, mins):
142
        if datetime.today() > self.timestamp + timedelta(minutes=mins):
143
            return True
144
        else:
145
            return False
146
147
# start ignoring translations required at runtime
148
def _(text): return text
149
150
class ForecastText:
151
152
    # translatable dictionaries
153
    conditions_text = {
154
        "0": _(u"Tornado"),
155
        "1": _(u"Tropical Storm"),
156
        "2": _(u"Hurricane"),
157
        "3": _(u"Severe Thunderstorms"),
158
        "4": _(u"Thunderstorms"),
159
        "5": _(u"Mixed Rain and Snow"),
160
        "6": _(u"Mixed Rain and Sleet"),
161
        "7": _(u"Mixed Precipitation"),
162
        "8": _(u"Freezing Drizzle"),
163
        "9": _(u"Drizzle"),
164
        "10": _(u"Freezing Rain"),"N/A"
165
        "11": _(u"Light Rain"),
166
        "12": _(u"Rain"),
167
        "13": _(u"Snow Flurries"),
168
        "14": _(u"Light Snow Showers"),
169
        "15": _(u"Drifting Snow"),
170
        "16": _(u"Snow"),
171
        "17": _(u"Hail"),
172
        "18": _(u"Sleet"),
173
        "19": _(u"Dust"),
174
        "20": _(u"Fog"),
175
        "21": _(u"Haze"),
176
        "22": _(u"Smoke"),
177
        "23": _(u"Blustery"),
178
        "24": _(u"Windy"),
179
        "25": _(u"N/A"),
180
        "26": _(u"Cloudy"),
181
        "27": _(u"Mostly Cloudy"),
182
        "28": _(u"Mostly Cloudy"),
183
        "29": _(u"Partly Cloudy"),
184
        "30": _(u"Partly Cloudy"),
185
        "31": _(u"Clear"),
186
        "32": _(u"Clear"),
187
        "33": _(u"Fair"),
188
        "34": _(u"Fair"),
189
        "35": _(u"Mixed Rain and Hail"),
190
        "36": _(u"Hot"),
191
        "37": _(u"Isolated Thunderstorms"),
192
        "38": _(u"Scattered Thunderstorms"),
193
        "39": _(u"Scattered Showers"),
194
        "40": _(u"Heavy Rain"),
195
        "41": _(u"Scattered Snow Showers"),
196
        "42": _(u"Heavy Snow"),
197
        "43": _(u"Heavy Snow"),
198
        "44": _(u"N/A"),
199
        "45": _(u"Scattered Showers"),
200
        "46": _(u"Snow Showers"),
201
        "47": _(u"Isolated Thunderstorms"),
202
        "na": _(u"N/A"),
203
        "-": _(u"N/A")
204
    }
205
206
    day_of_week_short = {
207
        "Today": _(u"Now"),
208
        "Monday": _(u"Mon"),
209
        "Tuesday": _(u"Tue"),
210
        "Wednesday": _(u"Wed"),
211
        "Thursday": _(u"Thu"),
212
        "Friday": _(u"Fri"),
213
        "Saturday": _(u"Sat"),
214
        "Sunday": _(u"Sun")
215
    }
216
    
217
    # this now returns ascii code
218
    bearing_arrow_font = {
219
        "S": 0x31,"N/A"
220
        "SSW": 0x32,
221
        "SW": 0x33,
222
        "WSW": 0x34,
223
        "W": 0x35,
224
        "WNW": 0x36,
225
        "NW": 0x37,
226
        "NNW": 0x38,
227
        "N": 0x39,
228
        "NNE": 0x3a,
229
        "NE": 0x3b,
230
        "ENE": 0x3c,
231
        "E": 0x3d,
232
        "ESE": 0x3e,
233
        "SE": 0x3f,
234
        "SSE": 0x40,
235
    }
236
237
    bearing_icon = {
238
        "calm": "00",
239
        "VAR": "01",
240
        "S": "05",
241
        "SSW": "06",
242
        "SW": "07",
243
        "WSW": "08",
244
        "W": "09",
245
        "WNW": "10",
246
        "NW": "11",
247
        "NNW": "12",
248
        "N": "13",
249
        "NNE": "14",
250
        "NE": "15",
251
        "ENE": "16",
252
        "E": "17",
253
        "ESE": "18",
254
        "SE": "19",
255
        "SSE": "20"
256
    }        
257
    
258
    # some general things...
259
    general = {
260
 	"n/a": _(u"n/a"),
261
	'N/A': _(u"N/A"),
262
	'Not Available': _(u"Not Available"),
263
	'unknown': _(u"unknown"),
264
	'NONE': _(u"NONE"),
265
	'day': _(u"day"),
266
	'night': _(u"night")
267
    }
268
    
269
    # UV index ...
270
    UV_index = {
271
	"Extreme": _(u"Extreme"),
272
	"Very high": _(u"Very High"),
273
	"High": _(u"High"),
274
	"Moderate": (u"Moderate"),
275
	"Low": _(u"Low")
276
    }
277
    
278
    # tendencies used for barometric pressure
279
    bar_pressure = {
280
	"Very Low": _(u"Very Low"),
281
	"Moderate": _(u"Moderate"),
282
	"rising": _(u"rising"),
283
	"falling": _(u"falling"),
284
	"steady": _(u"steady"),
285
	"calm": _(u"calm")
286
    }
287
288
289
    # wind directions long
290
    wind_directions_long = {
291
	"East": _(u"East"),
292
	"East Northeast": _(u"East Northeast"),
293
	"East Southeast": _(u"East Southeast"),
294
	"North": _(u"North"),
295
	"Northeast": _(u"Northeast"),
296
	"North Northeast": _(u"North Northeast"),
297
	"North Northwest": _(u"North Northwest"),
298
	"Northwest": _(u"Northwest"),
299
	"South": _(u"South"),
300
	"Souteast": _(u"Southeast"),
301
	"South Southeast": _(u"South Southeast"),"N/A"
302
	"South Southwest": _(u"South Southwest"),
303
	"Southwest": _(u"Southwest"),
304
	"variable": _(u"variable"),
305
	"West": _(u"West"),
306
	"West Northwest": _(u"West Northwest"),
307
	"West Southwest": _(u"West Southwest")
308
    }
309
    
310
    wind_directions_short = {
311
	"E": _(u"E"),
312
	"ENE": _(u"ENE"),
313
	"ESE": _(u"ESE"),
314
	"N": _(u"N"),
315
	"NE": _(u"NE"),
316
	"NNE": _(u"NNE"),
317
	"NNW": _(u"NNW"),
318
	"NW": _(u"NW"),
319
	"S": _(u"S"),
320
	"SE": _(u"SE"),
321
	"SSE": _(u"SSE"),
322
	"SSW": _(u"SSW"),
323
	"SW": _(u"SW"),
324
	"W": _(u"W"),
325
	"WNW": _(u"WNW"),
326
	"WSW": _(u"WSW")
327
    }
328
329
    days_of_week = {
330
	"Today": _(u"Today"),
331
	"Monday": _(u"Monday"),
332
	"Tuesday":_(u"Tuesday"),
333
	"Wednesday":_(u"Wednesday"),
334
	"Thursday": _(u"Thursday"),
335
	"Friday": _(u"Friday"),"N/A"
336
	"Saturday": _(u"Saturday"),
337
	"Sunday": _(u"Sunday")
338
    }
339
    
340
# end ignoring translations
341
del _
342
343
class ForecastInfo:
344
    
345
    # design time variables
346
    options = None
347
    config = None
348
    forecast_data = {}
349
    # a list of locations for which an attempt was made to load them
350
    # locations in this list are not loaded again (if there was an error,
351
    # this makes sure it doesn't reapeat over and over)
352
    loaded_locations = []
353
    error = ""
354
    errorfound = False
355
    
356
    # design time settings
357
    CACHE_FILENAME = ".conkyForecast-<LOCATION>.cache"
358
    CONDITION_IMAGE_FILENAME = ".conkyForecast-WI-<CONDITION>.gif"
359
360
    def __init__(self, options):
361
362
        self.options = options
363
                                         
364
        self.loadConfigData()
365
        
366
        # setup timeout for connections
367
        # TODO: seems like this doesn't work in all cases..
368
        socket.setdefaulttimeout(self.config.CONNECTION_TIMEOUT)
369
        
370
        # set the locale
371
        if self.options.locale == None:
372
            if self.config.LOCALE == "":
373
                self.options.locale = locale.getdefaultlocale()[0][0:2]
374
            else:
375
                self.options.locale = self.config.LOCALE
376
377
        self.logInfo("Locale set to " + self.options.locale)
378
        
379
        # if not the default "en" locale, configure the i18n language translation    
380
        if self.options.locale != "en":
381
382
            self.logInfo("Looking for translation file for '%s' under %s" % (self.options.locale, localedirectory))
383
            
384
            if gettext.find(domain, localedirectory, languages=[self.options.locale]) != None:
385
                self.logInfo("Translation file found for '%s'" % self.options.locale)
386
                
387
                try:
388
                    trans = gettext.translation(domain, localedirectory, languages=[self.options.locale])
389
                    trans.install(unicode=True)
390
                    self.logInfo("Translation installed for '%s'" % self.options.locale)
391
                    
392
                except Exception, e:
393
                    self.logError("Unable to load translation for '%s' %s" % (self.options.locale, e.__str__()))
394
            else:
395
                self.logInfo("Translation file not found for '%s', defaulting to 'en'" % self.options.locale)
396
                self.options.locale = "en"
397
398
        # setup location code if not set
399
        if self.options.location == None:
400
            self.options.location = self.config.DEFAULT_LOCATION           
401
        
402
        # setup a proxy if defined
403
        if self.config.PROXY_HOST != None:
404
            if self.config.PROXY_USERNAME != None and self.config.PROXY_PASSWORD != None:
405
                self.logInfo("Setting up proxy '%s:%d', with username and password"%(self.config.PROXY_HOST,self.config.PROXY_PORT))
406
                proxyurl = "http://%s:%s@%s:%d"%(self.config.PROXY_USERNAME,self.config.PROXY_PASSWORD,self.config.PROXY_HOST,self.config.PROXY_PORT)
407
            else:
408
                self.logInfo("Setting up proxy '%s:%d', without username and password"%(self.config.PROXY_HOST,self.config.PROXY_PORT))
409
                proxyurl = "http://%s:%d"%(self.config.PROXY_HOST,self.config.PROXY_PORT)
410
411
            try:
412
                proxy_support = urllib2.ProxyHandler({"http" : proxyurl})
413
                opener = urllib2.build_opener(proxy_support, urllib2.HTTPHandler)
414
                urllib2.install_opener(opener)
415
            except Exception, e:
416
                self.logError("Unable to setup proxy: %s"%e.__str__())
417
        else:
418
            self.logInfo("Not using a proxy as none is defined")
419
420
        # Check if the location is loaded, if not, load it. If it can't be loaded, there was an error
421
        if not self.checkAndLoad(self.options.location):
422
            self.logError("Failed to load the location cache")
423
                    
424
    def loadConfigData(self):
425
        try:         
426
            # load .conkyForecast.config from the options setting
427
            configfilepath = os.path.expanduser(self.options.config)
428
                                          
429
            if os.path.exists(configfilepath):
430
                
431
                self.config = ForecastConfig()
432
                
433
                #load the file
434
                fileinput = open(configfilepath)
435
                lines = fileinput.read().split("\n")
436
                fileinput.close() 
437
438
                for line in lines:
439
                    line = line.strip()
440
                    if len(line) > 0 and line[0:1] != "#": # ignore commented lines or empty ones
441
442
                        splitpos = line.find("=")
443
                        name = line[:splitpos-1].strip().upper() # config setting name on the left of =
444
                        value = line[splitpos+1:].split("#")[0].strip()
445
                        
446
                        if len(value) > 0:
447
                            if name == "CACHE_FOLDERPATH":
448
                                self.config.CACHE_FOLDERPATH = value
449
                            elif name == "CONNECTION_TIMEOUT":
450
                                try: # NITE: removed the isNumeric() check in favor of this..its more effective and lets the user know something is wrong
451
                                    self.config.CONNECTION_TIMEOUT = int(value)
452
                                except:
453
                                    self.logError("Invalid value of config option CONNECTION_TIMEOUT: " + value)
454
                            elif name == "EXPIRY_MINUTES":
455
                                try:
456
                                    self.config.EXPIRY_MINUTES = int(value)
457
                                except:
458
                                    self.logError("Invalid value of config option EXPIRY_MINUTES: " + value)
459
                            elif name == "TIME_FORMAT":
460
                                self.config.TIME_FORMAT = value
461
                            elif name == "DATE_FORMAT":
462
                                self.config.DATE_FORMAT = value                                    
463
                            elif name == "LOCALE":
464
                                self.config.LOCALE = value
465
                            elif name == "WU_LICENCE_KEY":
466
                                self.config.WU_LICENCE_KEY = value
467
                            elif name == "DEFAULT_LOCATION":
468
                                self.config.DEFAULT_LOCATION = value
469
                            elif name == "MAXIMUM_DAYS_FORECAST":
470
                                self.config.MAXIMUM_DAYS_FORECAST = int(value)
471
                            elif name == "AUTO_NIGHT":
472
                                self.config.AUTO_NIGHT = self.parseBoolString(value)
473
                            elif name == "BASE_WU_JSON_URL":
474
                                self.config.BASE_WU_JSON_URL = value
475
                            elif name == "PROXY_HOST":
476
                                self.config.PROXY_HOST = value
477
                            elif name == "PROXY_PORT":
478
                                self.config.PROXY_PORT = int(value)
479
                            elif name == "PROXY_USERNAME":
480
                                self.config.PROXY_USERNAME = value
481
                            elif name == "PROXY_PASSWORD":
482
                                self.config.PROXY_PASSWORD = value
483
                            else:
484
                                self.logError("Unknown option in config file: " + name)
485
486
                if self.options.verbose == True:
487
                    print >> sys.stdout,"*** CONFIG OPTIONS:"
488
                    print >> sys.stdout,"    CACHE_FOLDERPATH:", self.config.CACHE_FOLDERPATH
489
                    print >> sys.stdout,"    CONNECTION_TIMEOUT:", self.config.CONNECTION_TIMEOUT
490
                    print >> sys.stdout,"    EXPIRY_MINUTES:", self.config.EXPIRY_MINUTES
491
                    print >> sys.stdout,"    TIME_FORMAT:", self.config.TIME_FORMAT
492
                    print >> sys.stdout,"    DATE_FORMAT:", self.config.DATE_FORMAT                
493
                    print >> sys.stdout,"    LOCALE:", self.config.LOCALE
494
                    print >> sys.stdout,"    WU_LICENCE_KEY:", self.config.WU_LICENCE_KEY
495
                    print >> sys.stdout,"    DEFAULT_LOCATION:", self.config.DEFAULT_LOCATION
496
                    print >> sys.stdout,"    MAXIMUM_DAYS_FORECAST:", self.config.MAXIMUM_DAYS_FORECAST
497
                    print >> sys.stdout,"    BASE_WU_JSON_URL:", self.config.BASE_WU_JSON_URL
498
                    
499
            else:
500
                self.logError("Config data file %s not found, using defaults (Registration info is needed though)" % configfilepath)
501
502
        except Exception, e:
503
            self.logError("Error while loading config data, using defaults (Registration info is needed though): " + e.__str__())
504
505
506
    def checkAndLoad(self, location):
507
508
        # if the location was not loaded before (or attempted to load)
509
        if not location in self.loaded_locations:
510
            # add it to the list so it doesn't get loaded again (or attempted to load)
511
            self.loaded_locations.append(location)
512
513
            # define CACHE_FILEPATH based on cache folder and location code
514
            CACHE_FILEPATH = os.path.join(self.config.CACHE_FOLDERPATH, self.CACHE_FILENAME.replace("<LOCATION>", location.replace("/","-")))
515
516
            if not self.forecast_data.has_key(location):
517
                if os.path.exists(CACHE_FILEPATH):
518
                    try:
519
                        self.logInfo("Loading cache file " + CACHE_FILEPATH)
520
                        file = open(CACHE_FILEPATH, 'rb')
521
                        self.forecast_data[location] = pickle.load(file)
522
                        file.close()
523
                    except Exception, e:
524
                        self.logError("Unable to read the cache file %s: %s" % (CACHE_FILEPATH, e.__str__()))
525
                        #TODO: get to the bottom of failure to load pickled cache file, is this a 2.7.1 issue?
526
                        self.logInfo("Deleting cache file due to loading issues, it will be prepared again")
527
                        os.remove(CACHE_FILEPATH)
528
                        #return False
529
        
530
            # check the data in the dictionary and update if outdated
531
            # if there was an update, store the new data in the cache file
532
            if self.checkAndUpdate(location) == True:
533
                try:
534
                    self.logInfo("Saving updated cache file " + CACHE_FILEPATH)
535
                    file = open(CACHE_FILEPATH, 'wb')
536
                    pickle.dump(self.forecast_data[location], file)
537
                    file.close()
538
                except Exception, e:
539
                    self.logError("Unable to save cache file %s: %s" % (CACHE_FILEPATH, e.__str__()))
540
                    return False
541
542
        # if the location is still not in cache, print an error and return false to writeOutput()
543
        if self.forecast_data.has_key(location):
544
            return True
545
        else:
546
            self.logError("Location %s is not in cache." % self.options.location) 
547
            return False
548
        
549
550
    def checkAndUpdate(self, location):
551
        # if the location is outdated or the refetch is forced..
552
        if not self.forecast_data.has_key(location) or \
553
           self.forecast_data[location].outdated(self.config.EXPIRY_MINUTES) or \
554
           self.options.refetch == True:
555
556
            # obtain current conditions data from xoap service
557
            try:
558
                url = self.config.BASE_WU_JSON_URL.replace("<LOCATION>",location).replace("<WU_LICENCE_KEY>",self.config.WU_LICENCE_KEY)
559
                       
560
                self.logInfo("Fetching weather data from " + url)
561
562
                usock = urllib2.urlopen(url)
563
                jsondata = usock.read()
564
                usock.close()
565
566
            except Exception, e:
567
                self.logError("Server connection error: " + e.__str__())
568
                return False
569
            
570
            else:
571
                # interrogate weather data
572
                try:
573
                    # parse the XML
574
                    self.weatherdic = json.loads(jsondata)
575
576
                    current = self.weatherdic["current_observation"]                    
577
                    
578
                    city = current["display_location"]["city"]
579
                    country = current["display_location"]["country"]
580
                    latitude = current["display_location"]["latitude"]
581
                    longitude = current["display_location"]["longitude"]
582
583
                    observatory = current["observation_location"]["city"]
584
                    timezone = current["local_tz_short"]
585
                    
586
                    condition_url = current["icon_url"]
587
                    condition_text = current["weather"]
588
589
                    current_temp = str(current["temp_c"])
590
                    
591
        
592
                    last_update = current["observation_time_rfc822"]
593
                    
594
                    day_of_week = "Today"
595
                    current_temp_feels = str(current["windchill_c"])
596
                    
597
                    precip = _(u"N/A")  
598
                    wind_direction = current["wind_dir"]
599
                    wind_direction_numeric = str(current["wind_degrees"])
600
                    wind_speed = str(current["wind_mph"])
601
                    wind_gusts = str(current["wind_gust_mph"])
602
                    humidity = current["relative_humidity"]                    
603
                    
604
                    bar_read = str(current["pressure_mb"])
605
                    bar_desc = current["pressure_trend"]
606
                    uv_index = str(current["heat_index_c"])
607
                    uv_text = current["heat_index_string"]
608
                    dew_point = str(current["dewpoint_c"])
609
                    visibility = str(current["visibility_km"])
610
611
                    current_forecast_data = ForecastDataset(last_update, day_of_week, current_temp_feels, current_temp, condition_url, condition_text, precip, humidity, wind_direction, wind_direction_numeric, wind_speed, wind_gusts, timezone, bar_read, bar_desc, uv_index, uv_text, dew_point, observatory, visibility, city, country)
612
                    
613
                    bar_read = _(u"N/A")
614
                    bar_desc = _(u"N/A")
615
                    visibility = _(u"N/A")
616
                    uv_index = _(u"N/A")
617
                    uv_text = _(u"N/A")
618
                    dew_point = _(u"N/A")
619
                    
620
                    day_forecast_data_list = []
621
                    night_forecast_data_list = []
622
623
                    forecastlist = self.weatherdic["forecast"]["simpleforecast"]["forecastday"]
624
                    for forecastday in forecastlist:
625
                        last_update = time.strftime('%Y-%m-%d %I:%M:%S %p %Z',time.localtime(float(forecastday['date']['epoch'])))
626
                        daynumber = forecastday["period"]
627
                        day_of_week = forecastday["date"]["weekday"]
628
                        high_temp = str(forecastday["high"]["celsius"])
629
                        low_temp = str(forecastday["low"]["celsius"])                        
630
631
                        condition_url = forecastday["icon_url"]
632
                        condition_text = forecastday["conditions"]                        
633
                        
634
                        precip = str(forecastday["pop"])
635
                        wind_speed = _(u"N/A")
636
                        wind_gusts = _(u"N/A")
637
                        wind_direction_numeric = _(u"N/A")
638
                        wind_direction = _(u"N/A")
639
                        humidity = _(u"N/A")
640
        
641
                        
642
                        day_forecast_data = ForecastDataset(last_update, day_of_week, low_temp, high_temp, condition_url, condition_text, precip, humidity, wind_direction, wind_direction_numeric, wind_speed, wind_gusts, timezone, bar_read, bar_desc, uv_index, uv_text, dew_point, observatory, visibility, city, country)
643
                        day_forecast_data_list.append(day_forecast_data)
644
    
645
                        # no night data available at the mo....so just populating with day data
646
                        night_forecast_data = ForecastDataset(last_update, day_of_week, low_temp, high_temp, condition_url, condition_text, precip, humidity, wind_direction, wind_direction_numeric, wind_speed, wind_gusts, timezone, bar_read, bar_desc, uv_index, uv_text, dew_point, observatory, visibility, city, country)
647
                        night_forecast_data_list.append(night_forecast_data)
648
                        
649
                    self.forecast_data[location] = ForecastLocation(current_forecast_data, day_forecast_data_list, night_forecast_data_list)
650
    
651
                    return True
652
            
653
                except Exception, e:
654
                    self.logError("Error reading weather data: " + e.__str__())
655
                    return False
656
657
    def getTimestampOutput(self, timestamp, minuteshide):            
658
        # minuteshide:
659
        # None = disabled
660
        # -1 = hide days and use config.EXPIRY_MINUTES
661
        # 0 = hide days and always show hours
662
        
663
        output = u""
664
        
665
        today = datetime.today()
666
        days = today.day - timestamp.day
667
        if days or minuteshide == None:
668
            output += timestamp.strftime(self.config.DATE_FORMAT)
669
        
670
        if minuteshide == -1:
671
            minuteshide = self.config.EXPIRY_MINUTES
672
            
673
        delta = today - timestamp
674
        if days or minuteshide == None or minuteshide == 0 or delta.seconds > minuteshide * 60:
675
            if (len(output) > 0):
676
                output += " "
677
            output += timestamp.strftime(self.config.TIME_FORMAT)
678
        
679
        return output
680
681
682
    def getDatatypeFromSet(self, location, datatype, set, shortweekday, imperial, beaufort, metrespersecond, tempunit, speedunit, distanceunit, pressureunit, minuteshide, centeredwidth):
683
        output = u""
684
685
        try:
686
            if datatype == "LU":
687
                output = set.last_update.strip()                    
688
            elif datatype == "LF":
689
                output = self.getTimestampOutput(self.forecast_data[location].timestamp, minuteshide)
690
            elif datatype == "DW":
691
                if shortweekday == True:
692
                    output = _(ForecastText.day_of_week_short[set.day_of_week])
693
                else:
694
                    output = _(set.day_of_week)
695
            elif datatype == "WI":
696
                output = self.getImageSrcForCondition(set.condition_url, set.condition_text)
697
            elif datatype == "LT":
698
                if self.isNumeric(set.low) == True:
699
                    if imperial == True:
700
                        string = self.convertCelsiusToFahrenheit(set.low)
701
                    else:
702
                        string = set.low
703
                    string = string + tempunit
704
                else:
705
                    string = _(set.low)
706
                output = string
707
            elif datatype == "HT":
708
                if self.isNumeric(set.high) == True:
709
                    if imperial == True:
710
                        string = self.convertCelsiusToFahrenheit(set.high)
711
                    else:
712
                        string = set.high
713
                    string = string + tempunit
714
                else:
715
                    string = _(set.high)
716
                output = string                      
717
            elif datatype == "CT":
718
                output = _(set.condition_text)
719
            elif datatype == "PC":
720
                if self.isNumeric(set.precip) == True:
721
                    string = set.precip + u"%"
722
                else:
723
                    string = _(set.precip)
724
                output = string
725
            elif datatype == "HM":
726
                if self.isNumeric(set.humidity) == True:
727
                    string = set.humidity + u"%"
728
                else:
729
                    string = _(set.humidity)
730
                output = string
731
            elif datatype == "WD":
732
                output = _(set.wind_dir)
733
            elif datatype == "BF":
734
                if set.wind_speed.lower() == "calm":
735
                    output = chr(0x25)
736
                else:
737
                    if (set.wind_dir == "VAR"):
738
                        output = chr(0x22) # 2nd level var arrow
739
                    else:
740
                        try:
741
                            # for the old datatype, add 0x10, that makes the output in the A-P range,
742
                            # which is the 2nd level arrow
743
                            output = chr(ForecastText.bearing_arrow_font[set.wind_dir] + 0x10)
744
                        except KeyError:
745
                            # if the value wasn't found in ForecastText.bearing_arrow_font, use space
746
                            output = "-"
747
            elif datatype == "BS":
748
                if set.wind_speed.lower() == "calm":
749
                    output = chr(0x25)
750
                elif self.isNumeric(set.wind_speed) == True:
751
                    if (set.wind_dir == "VAR"):
752
                        output = chr(0x21 + self.getWindLevel(set.wind_speed))
753
                    else:
754
                        try:
755
                            output = chr(ForecastText.bearing_arrow_font[set.wind_dir] + self.getWindLevel(set.wind_speed) * 0x10)
756
                        except KeyError:
757
                            # if the value wasn't found in ForecastText.bearing_arrow_font, use N/A
758
                            output = "-"
759
                else:
760
                    try:
761
                        # if the speed is not "calm" but also not a number, add 0x10
762
                        # that makes the output in the A-P range, the 2nd level arrow
763
                        output = chr(ForecastText.bearing_arrow_font[set.wind_dir] + 0x10)
764
                    except KeyError:
765
                        # if the value wasn't found in ForecastText.bearing_arrow_font, use N/A
766
                        output = "-"
767
            elif datatype == "BI":
768
                if set.wind_speed.lower() == "calm":
769
                    output = self.getImagePathForBearing(ForecastText.bearing_icon["calm"])
770
                elif self.isNumeric(set.wind_speed) == True:
771
                    if (set.wind_dir == "VAR"):
772
                        output = self.getImagePathForBearing(int(ForecastText.bearing_icon[set.wind_dir]) + self.getWindLevel(set.wind_speed))
773
                    else:
774
                        try:
775
                            output = self.getImagePathForBearing(int(ForecastText.bearing_icon[set.wind_dir]) + self.getWindLevel(set.wind_speed)*16)
776
                        except KeyError:
777
                            # if the value wasn't found in ForecastText.bearing_icon, use calm code
778
                            output = self.getImagePathForBearing(ForecastText.bearing_icon["calm"])
779
                                        
780
            elif datatype == "WA":
781
                output = _(set.wind_dir_numeric)
782
            elif datatype == "WS":
783
                if self.isNumeric(set.wind_speed) == True:
784
                    if beaufort == True:
785
                        string = self.convertMPHtoBeaufort(set.wind_speed)
786
                    elif metrespersecond == True:
787
                        string = self.convertMPHtoMS(set.wind_speed)
788
                    elif imperial == True:
789
                        string = set.wind_speed
790
                    else:
791
                        string = self.convertMilesToKilometres(set.wind_speed)
792
                        
793
                    string = string + speedunit
794
                else:
795
                    string = _(set.wind_speed.lower())
796
                output = string
797
            elif datatype == "WG":
798
                if self.isNumeric(set.wind_gusts) == True:
799
                    if beaufort == True:
800
                        string = self.convertMPHtoBeaufort(set.wind_gusts)
801
                    elif metrespersecond == True:
802
                        string = self.convertMPHtoMS(set.wind_gusts)
803
                    elif imperial == True:
804
                        string = set.wind_gusts
805
                    else:
806
                        string = self.convertMilesToKilometres(set.wind_gusts)
807
808
                    string = string + speedunit
809
                else:
810
                    string = _(set.wind_gusts) # need to define translations
811
                output = string              
812
            elif datatype == "BR":
813
                if self.isNumeric(set.bar_read) == True:
814
                    if imperial == True:
815
                        string = self.convertMillibarsToInches(set.bar_read,2)
816
                    else:
817
                        string = set.bar_read
818
                    string = string + pressureunit
819
                else:
820
                    string = _(set.bar_read)
821
                output = string
822
            elif datatype == "BD":
823
                output = _(set.bar_desc) # need to define translations
824
            elif datatype == "UI":
825
                output = _(set.uv_index)
826
            elif datatype == "UT":
827
                output = _(set.uv_text)
828
            elif datatype == "DP":
829
                if self.isNumeric(set.dew_point) == True:
830
                    if imperial == True:
831
                        string = self.convertCelsiusToFahrenheit(set.dew_point)
832
                    else:
833
                        string = set.dew_point
834
                    string = string + tempunit
835
                else:
836
                    string = _(set.dew_point)
837
                output = string
838
            elif datatype == "OB":
839
                output = set.observatory
840
            elif datatype == "VI":
841
                if self.isNumeric(set.visibility) == True:
842
                    if imperial == True:
843
                        string = self.convertKilometresToMiles(set.visibility,1)
844
                    else:
845
                        string = set.visibility
846
                    string = string + distanceunit
847
                else:
848
                    string = _(set.visibility)
849
                output = string            
850
            elif datatype == "CN":
851
                output = set.city
852
            elif datatype == "CO":
853
                output = set.country
854
            else:
855
                self.logError("Unknown datatype requested: " + datatype)
856
857
        except KeyError, e:
858
            self.logError("Unknown value %s encountered for datatype '%s'! Please report this!" % (e.__str__(), datatype))
859
        
860
        # set the width if it is set, either left trimming or centering the text in spaces as requested
861
        if centeredwidth != None and self.isNumeric(centeredwidth) == True:
862
            if centeredwidth < len(output):
863
                output = output[:centeredwidth]
864
            else:
865
                output = output.center(int(centeredwidth))    
866
                            
867
        return output
868
869
870
    def getDatasetOutput(self, location, datatype, startday, endday, night, shortweekday, imperial, beaufort, metrespersecond, hideunits, hidedegreesymbol, spaces, minuteshide, centeredwidth):
871
872
        output = u""
873
874
        # Check if the location is loaded, if not, load it. If it can't be loaded, there was an error
875
        if not self.checkAndLoad(location):
876
            self.logError("Failed to load the location cache")
877
            return u""
878
        
879
        # define current units for output
880
        if hideunits == False:
881
            if imperial == False:
882
                tempunit = _(u"°C")
883
                speedunit = _(u"kph")
884
                distanceunit = _(u"km")
885
                pressureunit = _(u"mb")
886
            else:
887
                tempunit = _(u"°F")
888
                speedunit = _(u"mph")
889
                distanceunit = _(u"m")
890
                pressureunit = _(u"in")
891
                
892
            # override speed units if beaufort selected
893
            if beaufort == True:
894
                speedunit = u""
895
                
896
            if metrespersecond == True:
897
                speedunit = u"m/s"
898
        else:
899
            # remove degree symbol if not required
900
            if hidedegreesymbol == False:
901
                tempunit = u"°"
902
            else:
903
                tempunit = u""
904
                
905
            speedunit = u""
906
            distanceunit = u""
907
            pressureunit = u""
908
909
        if startday == None:
910
            output += self.getDatatypeFromSet(location, datatype, self.forecast_data[location].current, shortweekday, imperial, beaufort, metrespersecond, tempunit, speedunit, distanceunit, pressureunit, minuteshide, centeredwidth)
911
        else: # forecast data
912
913
            # ensure startday and enday are within the forecast limit
914
            
915
            if startday < 0:
916
                startday = 0
917
                self.logError("--startday set beyond forecast limit, reset to minimum of 0")
918
            elif startday > self.config.MAXIMUM_DAYS_FORECAST:
919
                startday = self.config.MAXIMUM_DAYS_FORECAST
920
                self.logError("--startday set beyond forecast limit, reset to maximum of " + str(self.config.MAXIMUM_DAYS_FORECAST))
921
                
922
            if endday == None: # if no endday was set use startday
923
                endday = startday
924
            elif endday < 0:
925
                endday = 0
926
                self.logError("--endday set beyond forecast limit, reset to minimum of 0")
927
            elif endday > self.config.MAXIMUM_DAYS_FORECAST:
928
                endday = self.config.MAXIMUM_DAYS_FORECAST
929
                self.logError("--endday set beyond forecast limit, reset to maximum of " + str(self.config.MAXIMUM_DAYS_FORECAST))
930
                
931
            for daynumber in range(startday, endday + 1):
932
                
933
                # if AUTO_NIGHT config is true then handle N/A output, by using the night option between 2pm and 2am, when the startday = 0. 
934
                if self.config.AUTO_NIGHT == True and daynumber == 0:
935
                    now = datetime.now()
936
                    hour = now.hour
937
                    if hour > 13 or hour < 2:
938
                        night = True
939
                
940
                if night == True:
941
                    output += self.getDatatypeFromSet(location, datatype, self.forecast_data[location].night[daynumber], shortweekday, imperial, beaufort, metrespersecond, tempunit, speedunit, distanceunit, pressureunit, minuteshide, centeredwidth)
942
                else:
943
                    output += self.getDatatypeFromSet(location, datatype, self.forecast_data[location].day[daynumber], shortweekday, imperial, beaufort, metrespersecond, tempunit, speedunit, distanceunit, pressureunit, minuteshide, centeredwidth)
944
                    
945
                if daynumber != endday:
946
                    output += self.getSpaces(spaces)
947
948
        return output
949
950
    def getTemplateItemOutput(self, template_text):
951
        
952
        # keys to template data
953
        LOCATION_KEY = "location"
954
        DATATYPE_KEY = "datatype"
955
        STARTDAY_KEY = "startday"
956
        ENDDAY_KEY = "endday"
957
        NIGHT_KEY = "night"
958
        SHORTWEEKDAY_KEY = "shortweekday"
959
        IMPERIAL_KEY = "imperial"
960
        BEAUFORT_KEY = "beaufort"
961
        METRESPERSECOND_KEY = "metrespersecond"
962
        HIDEUNITS_KEY = "hideunits"
963
        HIDEDEGREESYMBOL_KEY = "hidedegreesymbol"
964
        SPACES_KEY = "spaces"
965
        MINUTESHIDE_KEY = "minuteshide"
966
        CENTEREDWIDTH_KEY = "centeredwidth"
967
        
968
        location = self.options.location
969
        datatype = self.options.datatype
970
        startday = self.options.startday
971
        endday = self.options.endday
972
        night = self.options.night
973
        shortweekday = self.options.shortweekday
974
        imperial = self.options.imperial
975
        beaufort = self.options.beaufort
976
        metrespersecond = self.options.metrespersecond
977
        hideunits = self.options.hideunits
978
        hidedegreesymbol = self.options.hidedegreesymbol
979
        spaces = self.options.spaces
980
        minuteshide = self.options.minuteshide
981
        centeredwidth = self.options.centeredwidth
982
        
983
        for option in template_text.split('--'):
984
            if len(option) == 0 or option.isspace():
985
                continue
986
            
987
            # not using split here...it can't assign both key and value in one call, this should be faster
988
            x = option.find('=')
989
            if (x != -1):
990
                key = option[:x].strip()
991
                value = option[x + 1:].strip()
992
                if value == "":
993
                    value = None
994
            else:
995
                key = option.strip()
996
                value = None
997
            
998
            try:
999
                if key == LOCATION_KEY:
1000
                    location = value
1001
                elif key == DATATYPE_KEY:
1002
                    datatype = value
1003
                elif key == STARTDAY_KEY:
1004
                    startday = int(value)
1005
                elif key == ENDDAY_KEY:
1006
                    endday = int(value)
1007
                elif key == NIGHT_KEY:
1008
                    night = True
1009
                elif key == SHORTWEEKDAY_KEY:
1010
                    shortweekday = True
1011
                elif key == IMPERIAL_KEY:
1012
                    imperial = True
1013
                elif key == BEAUFORT_KEY:
1014
                    beaufort = True
1015
                elif key == METRESPERSECOND_KEY:
1016
                    metrespersecond = True
1017
                elif key == HIDEUNITS_KEY:
1018
                    hideunits = True
1019
                elif key == HIDEDEGREESYMBOL_KEY:
1020
                    hidedegreesymbol = True
1021
                elif key == SPACES_KEY:
1022
                    spaces = int(value)
1023
                elif key == MINUTESHIDE_KEY:
1024
                    if value != None:
1025
                        minuteshide = int(value)
1026
                    else:
1027
                        minuteshide = -1
1028
                elif key == CENTEREDWIDTH_KEY:
1029
                    centeredwidth = value
1030
                else:
1031
                    self.logError("Unknown template option: " + option)
1032
1033
            except (TypeError, ValueError):
1034
                self.logError("Cannot convert option argument to number: " + option)
1035
                return u""
1036
1037
        #REMOVED
1038
        # Check if the location is loaded, if not, load it. If it can't be loaded, there was an error
1039
        #if not self.checkAndLoad(location):
1040
        #    self.logError("Failed to load the location cache")
1041
        #    return u""
1042
        
1043
        if datatype != None:
1044
            return self.getDatasetOutput(location, datatype, startday, endday, night, shortweekday, imperial, beaufort, metrespersecond, hideunits, hidedegreesymbol, spaces, minuteshide, centeredwidth)
1045
        else:
1046
            self.logError("Template item does not have datatype defined")
1047
            return u""
1048
1049
    def getOutputFromTemplate(self, template):
1050
        output = u""
1051
        end = False
1052
        a = 0
1053
        
1054
        # a and b are indexes in the template string
1055
        # moving from left to right the string is processed
1056
        # b is index of the opening bracket and a of the closing bracket
1057
        # everything between b and a is a template that needs to be parsed
1058
        while not end:
1059
            b = template.find('[', a)
1060
            
1061
            if b == -1:
1062
                b = len(template)
1063
                end = True
1064
            
1065
            # if there is something between a and b, append it straight to output
1066
            if b > a:
1067
                output += template[a : b]
1068
                # check for the escape char (if we are not at the end)
1069
                if template[b - 1] == '\\' and not end:
1070
                    # if its there, replace it by the bracket
1071
                    output = output[:-1] + '['
1072
                    # skip the bracket in the input string and continue from the beginning
1073
                    a = b + 1
1074
                    continue
1075
                    
1076
            if end:
1077
                break
1078
            
1079
            a = template.find(']', b)
1080
            
1081
            if a == -1:
1082
                self.logError("Missing terminal bracket (]) for a template item")
1083
                return u""
1084
            
1085
            # if there is some template text...
1086
            if a > b + 1:
1087
                output += self.getTemplateItemOutput(template[b + 1 : a])
1088
            
1089
            a = a + 1
1090
1091
        return output
1092
1093
    def writeOutput(self):
1094
                
1095
        if self.options.template != None:
1096
            #load the file
1097
            try:
1098
                fileinput = codecs.open(os.path.expanduser(self.options.template), encoding='utf-8')
1099
                template = fileinput.read()
1100
                fileinput.close()
1101
            except Exception, e:
1102
                self.logError("Error loading template file: " + e.__str__())
1103
            else:
1104
                output = self.getOutputFromTemplate(template)
1105
        else:         
1106
            
1107
            output = self.getDatasetOutput(self.options.location, self.options.datatype, self.options.startday, self.options.endday, self.options.night, self.options.shortweekday, self.options.imperial, self.options.beaufort, self.options.metrespersecond, self.options.hideunits, self.options.hidedegreesymbol, self.options.spaces, self.options.minuteshide, self.options.centeredwidth)
1108
            
1109
        print output.encode("utf-8")
1110
1111
    def logInfo(self, text):
1112
        if self.options.verbose == True:
1113
            print >> sys.stdout, "INFO: " + text
1114
1115
        if self.options.infologfile != None:
1116
            datetimestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 
1117
            fileoutput = open(self.options.infologfile, "ab")
1118
            fileoutput.write(datetimestamp+" INFO: "+text+"\n")
1119
            fileoutput.close()
1120
            
1121
    def logError(self, text):
1122
        print >> sys.stderr, "ERROR: " + text
1123
        
1124
        if self.options.errorlogfile != None:
1125
            datetimestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 
1126
            fileoutput = open(self.options.errorlogfile, "ab")
1127
            fileoutput.write(datetimestamp+" ERROR: "+text+"\n")
1128
            fileoutput.close()
1129
1130
    def getText(self, parentNode, name):
1131
        try:
1132
            node = parentNode.getElementsByTagName(name)[0]
1133
        except IndexError:
1134
            raise Exception, "Data element <%s> not present under <%s>" % (name, parentNode.tagName)
1135
1136
        rc = ""
1137
        for child in node.childNodes:
1138
            if child.nodeType == child.TEXT_NODE:
1139
                rc = rc + child.data
1140
        return rc
1141
    
1142
    def getChild(self, parentNode, name, index = 0):
1143
        try:
1144
            return parentNode.getElementsByTagName(name)[index]
1145
        except IndexError:
1146
            raise Exception, "Data element <%s> is not present under <%s> (index %i)" % (name, parentNode.tagName, index)
1147
1148
    def getSpaces(self, spaces=1):
1149
        string = u""
1150
        for dummy in range(0, spaces):
1151
            string = string + u" "
1152
        return string
1153
1154
    def isNumeric(self, string):
1155
        try:
1156
            dummy = float(string)
1157
            return True
1158
        except:
1159
            return False
1160
        
1161
    def parseBoolString(self, string):
1162
        #return string[0].upper()=="T"
1163
        
1164
        if string is True or string is False:
1165
            return string
1166
        string = str(string).strip().lower()
1167
        return not string in ['false','f','n','0','']
1168
1169
    def convertCelsiusToFahrenheit(self, temp, dp=0):
1170
        value = ((float(temp) * 9.0) / 5.0) + 32
1171
        if dp == 0:
1172
            return str(int(round(value,dp))) # lose the dp
1173
        else:
1174
            return str(round(value,dp))
1175
1176
    def convertKilometresToMiles(self, dist, dp=0):
1177
        value = float(dist) * 0.621371192
1178
        if dp == 0:
1179
            return str(int(round(value,dp))) # lose the dp
1180
        else:
1181
            return str(round(value,dp))
1182
1183
    def convertMilesToKilometres(self, dist, dp=0):
1184
        value = float(dist) * 1.609344001
1185
        if dp == 0:
1186
            return str(int(round(value,dp))) # lose the dp
1187
        else:
1188
            return str(round(value,dp))
1189
        
1190
    def convertKPHtoBeaufort(self, kph, dp=0):
1191
        value = pow(float(kph) * 0.332270069, 2.0 / 3.0)
1192
        if dp == 0:
1193
            return str(int(round(value,dp))) # lose the dp
1194
        else:
1195
            return str(round(value,dp))
1196
    
1197
    def convertMPHtoBeaufort(self, mph, dp=0):
1198
        value = pow(float(self.convertMilesToKilometres(mph)) * 0.332270069, 2.0 / 3.0)
1199
        if dp == 0:
1200
            return str(int(round(value,dp))) # lose the dp
1201
        else:
1202
            return str(round(value,dp))
1203
            
1204
    def convertKPHtoMS(self, kph, dp=0):
1205
        value = float(kph) * 0.27777778
1206
        if dp == 0:
1207
            return str(int(round(value,dp))) # lose the dp
1208
        else:
1209
            return str(round(value,dp))
1210
1211
    def convertMPHtoMS(self, mph, dp=0):
1212
        value = float(self.convertMilesToKilometres(mph)) * 0.27777778
1213
        if dp == 0:
1214
            return str(int(round(value,dp))) # lose the dp
1215
        else:
1216
            return str(round(value,dp))
1217
                
1218
    def convertMillibarsToInches(self,mb,dp=0):
1219
        value = float(mb)/33.8582
1220
        if dp == 0:
1221
            return str(int(round(value,dp))) # lose the dp
1222
        else:
1223
            return str(round(value,dp))
1224
    
1225
    def getWindLevel(self, speed):
1226
        beaufort = int(self.convertMPHtoBeaufort(speed))
1227
        if beaufort < 4:
1228
            return 0
1229
        elif beaufort < 7:
1230
            return 1
1231
        elif beaufort < 10:
1232
            return 2
1233
        else:
1234
            return 3
1235
1236
    def getFormattedTimeFromSeconds(self,seconds,showseconds=False):
1237
        time = int(seconds)
1238
        hours, time = divmod(time, 60*60)
1239
        minutes, seconds = divmod(time, 60)
1240
        
1241
        if showseconds == True:
1242
            output = "%02d:%02d:%02d"%(hours, minutes, seconds)
1243
        else:
1244
            output = "%02d:%02d"%(hours, minutes)
1245
            
1246
        return output
1247
        
1248
    def getImageSrcForCondition(self, url, condition):
1249
        imagesrc = ""
1250
        imgfilepath = os.path.join(self.config.CACHE_FOLDERPATH, self.CONDITION_IMAGE_FILENAME.replace("<CONDITION>",condition.replace(" ","-")))       
1251
1252
        if os.path.exists(imgfilepath) == False:
1253
1254
            try:
1255
                self.logInfo("Fetching image from " + imagesrc)
1256
    
1257
                usock = urllib2.urlopen(url)
1258
                img = usock.read()
1259
            except Exception, e:
1260
                self.logError("Error downloading the image file: " + e.__str__()+"\n"+traceback.format_exc())
1261
            else:
1262
                # save the image and contruct an image tag
1263
                
1264
                imgfile = open(imgfilepath,'wb')
1265
                imgfile.write(img)
1266
                self.logInfo("Saved image to " + imgfilepath)
1267
     
1268
            finally:
1269
                usock.close()
1270
                imgfile.close()
1271
1272
        return imgfilepath
1273
    
1274
    def getImagePathForBearing(self, bearingcode):
1275
        #TODO: Once gif supported properly in conky re-enable gif output
1276
        #if int(bearingcode) > 0 and int(bearingcode) <= 4:
1277
        #    fileext = "gif" # use animated gif for VAR output
1278
        #else:
1279
        #    fileext = "png"
1280
            
1281
        fileext = "png" #force to always be png until animated gifs are supported
1282
        imagesrc = "%s/images/bearingicons/%s.%s"%(app_path, str(bearingcode).rjust(2,"0"),fileext)
1283
        return imagesrc
1284
    
1285
def main():
1286
1287
    parser = CommandLineParser()
1288
    (options, args) = parser.parse_args()
1289
1290
    if options.version == True:
1291
        
1292
        print >> sys.stdout,"conkyForecast v.2.21"
1293
        
1294
    else:
1295
        
1296
        if options.verbose == True:
1297
            print >> sys.stdout, "*** INITIAL OPTIONS:"
1298
            print >> sys.stdout, "    config:", options.config
1299
            print >> sys.stdout, "    location:", options.location
1300
            print >> sys.stdout, "    datatype:", options.datatype
1301
            print >> sys.stdout, "    start day:", options.startday
1302
            print >> sys.stdout, "    end day:", options.endday
1303
            print >> sys.stdout, "    spaces:", options.spaces
1304
            print >> sys.stdout, "    template:", options.template
1305
            print >> sys.stdout, "    locale:", options.locale
1306
            print >> sys.stdout, "    imperial:", options.imperial
1307
            print >> sys.stdout, "    beaufort:", options.beaufort
1308
            print >> sys.stdout, "    metrespersecond:", options.metrespersecond
1309
            print >> sys.stdout, "    night:", options.night
1310
            print >> sys.stdout, "    shortweekday:", options.shortweekday
1311
            print >> sys.stdout, "    hideunits:", options.hideunits
1312
            print >> sys.stdout, "    hidedegreesymbol:", options.hidedegreesymbol
1313
            print >> sys.stdout, "    minuteshide:", options.minuteshide
1314
            print >> sys.stdout, "    centeredwidth:", options.centeredwidth
1315
            print >> sys.stdout, "    refetch:", options.refetch
1316
            print >> sys.stdout, "    verbose:", options.verbose
1317
            print >> sys.stdout, "    errorlogfile:",options.errorlogfile
1318
            print >> sys.stdout, "    infologfile:",options.infologfile        
1319
    
1320
        forecastinfo = ForecastInfo(options)
1321
        forecastinfo.writeOutput()
1322
1323
if __name__ == '__main__':
1324
    main()
1325
    sys.exit()
1326