| 1 | #!/usr/bin/env python |
|---|
| 2 | # $HeadURL$ |
|---|
| 3 | # $Id$ |
|---|
| 4 | # vim: ft=python ts=2 sw=2 et: |
|---|
| 5 | |
|---|
| 6 | # Copyright Dan Cardamore <dan@hld.ca> |
|---|
| 7 | # Licensed under the GNU GPL version 2.0 |
|---|
| 8 | # See GPL.gz in source distribution for more information |
|---|
| 9 | |
|---|
| 10 | import sys,time,string,urllib2 |
|---|
| 11 | from threading import Timer |
|---|
| 12 | |
|---|
| 13 | import curses |
|---|
| 14 | import weatherfeed |
|---|
| 15 | |
|---|
| 16 | version = "0.6" |
|---|
| 17 | |
|---|
| 18 | class asciiIcons: |
|---|
| 19 | def __init__(self): |
|---|
| 20 | self.blank = """ |
|---|
| 21 | |
|---|
| 22 | |
|---|
| 23 | |
|---|
| 24 | |
|---|
| 25 | |
|---|
| 26 | |
|---|
| 27 | """ |
|---|
| 28 | |
|---|
| 29 | self.lightning = """ |
|---|
| 30 | |
|---|
| 31 | \\\\ |
|---|
| 32 | \\\\\\ |
|---|
| 33 | \\\\ |
|---|
| 34 | \\\ |
|---|
| 35 | \ |
|---|
| 36 | """ |
|---|
| 37 | |
|---|
| 38 | self.cloudy = """ |
|---|
| 39 | __ _ |
|---|
| 40 | /- \_/\---/ \ |
|---|
| 41 | | -- | |
|---|
| 42 | \-_/---\__--__-| |
|---|
| 43 | |
|---|
| 44 | |
|---|
| 45 | """ |
|---|
| 46 | |
|---|
| 47 | self.raining = """ |
|---|
| 48 | __ _ |
|---|
| 49 | /- \_/\---/ \ |
|---|
| 50 | | -- | |
|---|
| 51 | |______________| |
|---|
| 52 | |
|---|
| 53 | | | | | | | | |
|---|
| 54 | """ |
|---|
| 55 | |
|---|
| 56 | |
|---|
| 57 | self.unknown = """ |
|---|
| 58 | |
|---|
| 59 | |
|---|
| 60 | |
|---|
| 61 | |
|---|
| 62 | |
|---|
| 63 | |
|---|
| 64 | """ |
|---|
| 65 | |
|---|
| 66 | self.sunny = """ |
|---|
| 67 | \ ___ |
|---|
| 68 | / \ / |
|---|
| 69 | - | | - |
|---|
| 70 | \ ___ / - |
|---|
| 71 | / \ |
|---|
| 72 | | | |
|---|
| 73 | """ |
|---|
| 74 | |
|---|
| 75 | |
|---|
| 76 | self.clear = """ |
|---|
| 77 | ___ |
|---|
| 78 | / \ |
|---|
| 79 | | | |
|---|
| 80 | \ ___ / |
|---|
| 81 | |
|---|
| 82 | |
|---|
| 83 | """ |
|---|
| 84 | |
|---|
| 85 | self.partlycloudy = """ |
|---|
| 86 | \ --- ____ |
|---|
| 87 | - /....| \/ \ |
|---|
| 88 | - |...| | |
|---|
| 89 | / \___\ __ / |
|---|
| 90 | / | --/ \ |
|---|
| 91 | |
|---|
| 92 | """ |
|---|
| 93 | |
|---|
| 94 | def getIcon(self, type): |
|---|
| 95 | """Returns a string with an ascii icon 5 rows by 18 wide |
|---|
| 96 | type is a string which refers to the icon wanted""" |
|---|
| 97 | if type == "Partly Cloudy": |
|---|
| 98 | return self.partlycloudy |
|---|
| 99 | elif type == "Mostly Cloudy": |
|---|
| 100 | return self.partlycloudy |
|---|
| 101 | elif type == "Cloudy": |
|---|
| 102 | return self.cloudy |
|---|
| 103 | elif type == "Clear": |
|---|
| 104 | return self.clear |
|---|
| 105 | elif type == "Light Rain": |
|---|
| 106 | return self.raining |
|---|
| 107 | elif type == "Showers": |
|---|
| 108 | return self.raining |
|---|
| 109 | elif type == "AM Showers": |
|---|
| 110 | return self.raining |
|---|
| 111 | elif type == "PM Showers": |
|---|
| 112 | return self.raining |
|---|
| 113 | elif type == "Isolated T-Storms": |
|---|
| 114 | return self.lightning |
|---|
| 115 | elif type == "Mostly Sunny": |
|---|
| 116 | return self.sunny |
|---|
| 117 | elif type == "Sunny": |
|---|
| 118 | return self.sunny |
|---|
| 119 | else: |
|---|
| 120 | return self.unknown |
|---|
| 121 | |
|---|
| 122 | def initColors(): |
|---|
| 123 | try: |
|---|
| 124 | curses.start_color() |
|---|
| 125 | curses.use_default_colors() |
|---|
| 126 | curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) |
|---|
| 127 | curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_RED) |
|---|
| 128 | curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) |
|---|
| 129 | curses.init_pair(4, curses.COLOR_GREEN, curses.COLOR_BLACK) |
|---|
| 130 | except curses.error: pass |
|---|
| 131 | |
|---|
| 132 | def scnTitle(win): |
|---|
| 133 | win.clear() |
|---|
| 134 | maxy,maxx = win.getmaxyx() |
|---|
| 135 | maxx -= 2 |
|---|
| 136 | win.bkgd(" ",curses.color_pair(1)) |
|---|
| 137 | try: |
|---|
| 138 | win.addstr(0,1, |
|---|
| 139 | ("Location %s -- Updated: %s" % |
|---|
| 140 | (weather.currentConditions["cityname"], |
|---|
| 141 | weather.currentConditions["observed"] )).center(maxx), |
|---|
| 142 | curses.A_BOLD |
|---|
| 143 | ) |
|---|
| 144 | except curses.error: pass |
|---|
| 145 | win.refresh() |
|---|
| 146 | |
|---|
| 147 | def scnCurrent(win): |
|---|
| 148 | win.clear() |
|---|
| 149 | maxy,maxx = win.getmaxyx() |
|---|
| 150 | maxx -= 2 |
|---|
| 151 | line = 0 |
|---|
| 152 | icons = asciiIcons() |
|---|
| 153 | win.addstr(line,2, icons.getIcon(weather.currentConditions["type"]));line+=6 |
|---|
| 154 | win.addstr(line,2,("%s" %(weather.currentConditions["type"])).center(maxx),curses.color_pair(4));line+=2 |
|---|
| 155 | win.addstr(line,2," Temp: %s" %(weather.currentConditions["temperature"]));line+=1 |
|---|
| 156 | if weather.currentConditions["wind"]["speed"] == "calm": |
|---|
| 157 | win.addstr(line,2," Wind: Calm") ;line+=1 |
|---|
| 158 | else: |
|---|
| 159 | win.addstr(line,2," Wind: %s %s" %( weather.currentConditions["wind"]["speed"], |
|---|
| 160 | weather.currentConditions["wind"]["direction"]) |
|---|
| 161 | );line+=1 |
|---|
| 162 | win.addstr(line,2,"Visbility: %s" %(weather.currentConditions["visibility"]));line+=1 |
|---|
| 163 | win.addstr(line,2," Humidity: %s" %(weather.currentConditions["humidity"]));line+=1 |
|---|
| 164 | line += 1 |
|---|
| 165 | win.addstr(line,2," Sunrise: %s" %(weather.currentConditions["sunrise"]));line+=1 |
|---|
| 166 | win.addstr(line,2," Sunset: %s" %(weather.currentConditions["sunset"]));line+=1 |
|---|
| 167 | line += 1 |
|---|
| 168 | win.addstr(line,2," UV index: %s" %(weather.currentConditions["uv"]["index"]));line+=1 |
|---|
| 169 | win.addstr(line,2," risk: %s" %(weather.currentConditions["uv"]["risk"]));line+=1 |
|---|
| 170 | |
|---|
| 171 | win.box() |
|---|
| 172 | win.addstr(0,1,"Current Conditions",curses.color_pair(2)|curses.A_BOLD); line += 1 |
|---|
| 173 | win.refresh() |
|---|
| 174 | |
|---|
| 175 | def scnForecast(win): |
|---|
| 176 | maxy,maxx = win.getmaxyx() |
|---|
| 177 | maxx -= 2 |
|---|
| 178 | line = 0 |
|---|
| 179 | col = 0 |
|---|
| 180 | global dayWindows |
|---|
| 181 | global forecastWindowsCreated |
|---|
| 182 | if not forecastWindowsCreated: |
|---|
| 183 | forecastWindowsCreated = True |
|---|
| 184 | day = 0 |
|---|
| 185 | dayWindows = [] |
|---|
| 186 | while day < 5: |
|---|
| 187 | if day < 5: |
|---|
| 188 | try: |
|---|
| 189 | dayWindows.append(win.derwin(int(maxy/5),maxx,int(day*(maxy/5))+1,1)) |
|---|
| 190 | except curses.error: pass |
|---|
| 191 | else: |
|---|
| 192 | try: |
|---|
| 193 | dayWindows.append(win.derwin(int(maxy/5),maxx,int((day-5)*(maxy/5))+1,int(maxx/2)+1)) |
|---|
| 194 | except curses.error: pass |
|---|
| 195 | day += 1 |
|---|
| 196 | |
|---|
| 197 | day = 0 |
|---|
| 198 | while day < 5: |
|---|
| 199 | scnDay(day) |
|---|
| 200 | day+=1 |
|---|
| 201 | win.box() |
|---|
| 202 | try: |
|---|
| 203 | win.addstr(0,36,"Forecast Conditions",curses.color_pair(2)|curses.A_BOLD); line += 1 |
|---|
| 204 | except curses.error: pass |
|---|
| 205 | win.refresh() |
|---|
| 206 | |
|---|
| 207 | def scnDay(day): |
|---|
| 208 | global dayWindows |
|---|
| 209 | try: |
|---|
| 210 | win = dayWindows.pop(0) |
|---|
| 211 | except: |
|---|
| 212 | return #this one wasn't allocated a window so just return |
|---|
| 213 | win.clear() |
|---|
| 214 | maxy,maxx = win.getmaxyx() |
|---|
| 215 | maxx -= 13 |
|---|
| 216 | line = 1 |
|---|
| 217 | |
|---|
| 218 | try: |
|---|
| 219 | win.addstr(line,1,"Hi/Lo: %s/%s" %(weather.forecast[day]["high"], |
|---|
| 220 | weather.forecast[day]["low"]));line+=0 |
|---|
| 221 | except curses.error: pass |
|---|
| 222 | try: |
|---|
| 223 | win.addstr(line,maxx,"Wind:%s %s" %( weather.forecast[day]["day"]["wind"]["speed"], |
|---|
| 224 | weather.forecast[day]["day"]["wind"]["direction"]) |
|---|
| 225 | );line+=1 |
|---|
| 226 | except curses.error: pass |
|---|
| 227 | try: |
|---|
| 228 | win.addstr(line,1,"%s" %(weather.forecast[day]["day"]["type"]));line+=0 |
|---|
| 229 | except curses.error: pass |
|---|
| 230 | try: |
|---|
| 231 | win.addstr(line,maxx,"POP: %s" %(weather.forecast[day]["day"]["pop"]));line+=1 |
|---|
| 232 | except curses.error: pass |
|---|
| 233 | |
|---|
| 234 | try: |
|---|
| 235 | win.box() |
|---|
| 236 | except curses.error: pass |
|---|
| 237 | try: |
|---|
| 238 | win.addstr(0,2,"%s: %s"%(weather.forecast[day]["Date"],weather.forecast[day]["Day"]),curses.color_pair(3)) |
|---|
| 239 | except curses.error: pass |
|---|
| 240 | win.refresh() |
|---|
| 241 | |
|---|
| 242 | def quit(): |
|---|
| 243 | refreshTimer.cancel() |
|---|
| 244 | sys.exit(1) |
|---|
| 245 | |
|---|
| 246 | def update(stdscr): |
|---|
| 247 | global weather |
|---|
| 248 | while 1: |
|---|
| 249 | try: |
|---|
| 250 | weather = weatherfeed.Weather(location, metric) |
|---|
| 251 | except urllib2.URLError: |
|---|
| 252 | time.sleep(60) |
|---|
| 253 | continue |
|---|
| 254 | break |
|---|
| 255 | |
|---|
| 256 | scnTitle(twin) |
|---|
| 257 | scnCurrent(cwin) |
|---|
| 258 | scnForecast(fwin) |
|---|
| 259 | stdscr.refresh() |
|---|
| 260 | |
|---|
| 261 | del weather |
|---|
| 262 | global refreshTimer |
|---|
| 263 | try: |
|---|
| 264 | refreshTimer.cancel() |
|---|
| 265 | except: pass |
|---|
| 266 | refreshTimer = Timer(refresh * 60, update, [stdscr]) |
|---|
| 267 | refreshTimer.start() |
|---|
| 268 | |
|---|
| 269 | |
|---|
| 270 | def main(stdscr): |
|---|
| 271 | global cwin,twin,fwin |
|---|
| 272 | global twidth,theight |
|---|
| 273 | theight, twidth = stdscr.getmaxyx() |
|---|
| 274 | |
|---|
| 275 | global forecastWindowsCreated |
|---|
| 276 | forecastWindowsCreated = False |
|---|
| 277 | |
|---|
| 278 | initColors() |
|---|
| 279 | twin = stdscr.derwin(1,twidth,0,0) |
|---|
| 280 | cwin = stdscr.derwin(theight-1,int(0.3*twidth),1,0) |
|---|
| 281 | fwin = stdscr.derwin(theight-1,int(0.7*twidth),1,int(0.3*twidth)) |
|---|
| 282 | |
|---|
| 283 | update(stdscr) |
|---|
| 284 | stdscr.keypad(1) |
|---|
| 285 | while 1: |
|---|
| 286 | try: |
|---|
| 287 | c = stdscr.getch() |
|---|
| 288 | except: |
|---|
| 289 | quit() |
|---|
| 290 | if c == ord('q'): quit() # quit |
|---|
| 291 | else: |
|---|
| 292 | update(stdscr) |
|---|
| 293 | |
|---|
| 294 | def printVersion(): |
|---|
| 295 | print "ctw version %s"%(version) |
|---|
| 296 | print "weatherfeed.py backend version: %2.2f"%(weatherfeed.version) |
|---|
| 297 | |
|---|
| 298 | def usage(): |
|---|
| 299 | print """ |
|---|
| 300 | Welcome to "Curse the Weather" Version %s |
|---|
| 301 | This program will display the weather for a city on the console. |
|---|
| 302 | Copyright (c)2004 Dan Cardamore <dan@hld.ca> |
|---|
| 303 | |
|---|
| 304 | ctw [options] LOCATION |
|---|
| 305 | options: |
|---|
| 306 | --refresh=<minutes> : the delay in minutes to refresh. |
|---|
| 307 | -h, --help : this help text |
|---|
| 308 | -d : debug output |
|---|
| 309 | --version: prints the version of this application |
|---|
| 310 | --nometric: print information in imperial units |
|---|
| 311 | |
|---|
| 312 | To determine your location code (LOCATION): |
|---|
| 313 | 1. visit: http://www.weather.com |
|---|
| 314 | 2. get your local forecast |
|---|
| 315 | 3. look at the URL, it will look similar to: |
|---|
| 316 | http://www.weather.com/outlook/travel/local/CAXX0343?from=search_city |
|---|
| 317 | 4. Your location code is the part after "local/" and before "?from" |
|---|
| 318 | in this example it is CAXX0343 |
|---|
| 319 | |
|---|
| 320 | """ %(version) |
|---|
| 321 | |
|---|
| 322 | if __name__ == "__main__": |
|---|
| 323 | if weatherfeed.version < 0.2: |
|---|
| 324 | print "This version of ctw requires weatherfeed.py version 0.2 and above" |
|---|
| 325 | print "See: http://opensource.hld.ca/trac.cgi/wiki/CurseTheWeather" |
|---|
| 326 | print "to get the latest version." |
|---|
| 327 | sys.exit(1) |
|---|
| 328 | |
|---|
| 329 | import getopt |
|---|
| 330 | try: |
|---|
| 331 | opts, args = getopt.getopt(sys.argv[1:], |
|---|
| 332 | "hrv:d", ["help","refresh=","version","nometric"]) |
|---|
| 333 | except getopt.GetoptError: |
|---|
| 334 | usage() |
|---|
| 335 | sys.exit(2) |
|---|
| 336 | |
|---|
| 337 | global debug |
|---|
| 338 | debug = False |
|---|
| 339 | global location |
|---|
| 340 | location = "" |
|---|
| 341 | global refresh |
|---|
| 342 | refresh = 60 |
|---|
| 343 | global metric |
|---|
| 344 | metric = True |
|---|
| 345 | |
|---|
| 346 | for o, a in opts: |
|---|
| 347 | if o == "-d": |
|---|
| 348 | debug = True |
|---|
| 349 | if o in ("-v", "--version"): |
|---|
| 350 | printVersion() |
|---|
| 351 | sys.exit(1) |
|---|
| 352 | if o in ("-h", "--help"): |
|---|
| 353 | usage() |
|---|
| 354 | sys.exit() |
|---|
| 355 | if o in ("-r", "--refresh"): |
|---|
| 356 | refresh = int(a) |
|---|
| 357 | if o in ("--nometric"): |
|---|
| 358 | metric = False |
|---|
| 359 | |
|---|
| 360 | if len(args) != 1: |
|---|
| 361 | print "invalid location, or too many arguments" |
|---|
| 362 | usage() |
|---|
| 363 | sys.exit(2) |
|---|
| 364 | |
|---|
| 365 | for arg in args: |
|---|
| 366 | location = arg |
|---|
| 367 | |
|---|
| 368 | if location == "": |
|---|
| 369 | print "Invalid location!" |
|---|
| 370 | usage() |
|---|
| 371 | sys.exit(2) |
|---|
| 372 | |
|---|
| 373 | if refresh < 10: |
|---|
| 374 | print "Invalid refresh rate. Must be at least 10 minute" |
|---|
| 375 | usage() |
|---|
| 376 | sys.exit(2) |
|---|
| 377 | |
|---|
| 378 | curses.wrapper(main) |
|---|