summaryrefslogtreecommitdiffstats
path: root/powerline/segments/common/wthr.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--powerline/segments/common/wthr.py235
1 files changed, 235 insertions, 0 deletions
diff --git a/powerline/segments/common/wthr.py b/powerline/segments/common/wthr.py
new file mode 100644
index 0000000..1c6d080
--- /dev/null
+++ b/powerline/segments/common/wthr.py
@@ -0,0 +1,235 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+import json
+
+from powerline.lib.url import urllib_read, urllib_urlencode
+from powerline.lib.threaded import KwThreadedSegment
+from powerline.segments import with_docstring
+
+
+# XXX Warning: module name must not be equal to the segment name as long as this
+# segment is imported into powerline.segments.common module.
+
+
+# Weather condition code descriptions available at
+# http://developer.yahoo.com/weather/#codes
+weather_conditions_codes = (
+ ('tornado', 'stormy'), # 0
+ ('tropical_storm', 'stormy'), # 1
+ ('hurricane', 'stormy'), # 2
+ ('severe_thunderstorms', 'stormy'), # 3
+ ('thunderstorms', 'stormy'), # 4
+ ('mixed_rain_and_snow', 'rainy' ), # 5
+ ('mixed_rain_and_sleet', 'rainy' ), # 6
+ ('mixed_snow_and_sleet', 'snowy' ), # 7
+ ('freezing_drizzle', 'rainy' ), # 8
+ ('drizzle', 'rainy' ), # 9
+ ('freezing_rain', 'rainy' ), # 10
+ ('showers', 'rainy' ), # 11
+ ('showers', 'rainy' ), # 12
+ ('snow_flurries', 'snowy' ), # 13
+ ('light_snow_showers', 'snowy' ), # 14
+ ('blowing_snow', 'snowy' ), # 15
+ ('snow', 'snowy' ), # 16
+ ('hail', 'snowy' ), # 17
+ ('sleet', 'snowy' ), # 18
+ ('dust', 'foggy' ), # 19
+ ('fog', 'foggy' ), # 20
+ ('haze', 'foggy' ), # 21
+ ('smoky', 'foggy' ), # 22
+ ('blustery', 'windy' ), # 23
+ ('windy', ), # 24
+ ('cold', 'day' ), # 25
+ ('clouds', 'cloudy'), # 26
+ ('mostly_cloudy_night', 'cloudy'), # 27
+ ('mostly_cloudy_day', 'cloudy'), # 28
+ ('partly_cloudy_night', 'cloudy'), # 29
+ ('partly_cloudy_day', 'cloudy'), # 30
+ ('clear_night', 'night' ), # 31
+ ('sun', 'sunny' ), # 32
+ ('fair_night', 'night' ), # 33
+ ('fair_day', 'day' ), # 34
+ ('mixed_rain_and_hail', 'rainy' ), # 35
+ ('hot', 'sunny' ), # 36
+ ('isolated_thunderstorms', 'stormy'), # 37
+ ('scattered_thunderstorms', 'stormy'), # 38
+ ('scattered_thunderstorms', 'stormy'), # 39
+ ('scattered_showers', 'rainy' ), # 40
+ ('heavy_snow', 'snowy' ), # 41
+ ('scattered_snow_showers', 'snowy' ), # 42
+ ('heavy_snow', 'snowy' ), # 43
+ ('partly_cloudy', 'cloudy'), # 44
+ ('thundershowers', 'rainy' ), # 45
+ ('snow_showers', 'snowy' ), # 46
+ ('isolated_thundershowers', 'rainy' ), # 47
+)
+# ('day', (25, 34)),
+# ('rainy', (5, 6, 8, 9, 10, 11, 12, 35, 40, 45, 47)),
+# ('cloudy', (26, 27, 28, 29, 30, 44)),
+# ('snowy', (7, 13, 14, 15, 16, 17, 18, 41, 42, 43, 46)),
+# ('stormy', (0, 1, 2, 3, 4, 37, 38, 39)),
+# ('foggy', (19, 20, 21, 22, 23)),
+# ('sunny', (32, 36)),
+# ('night', (31, 33))):
+weather_conditions_icons = {
+ 'day': 'DAY',
+ 'blustery': 'WIND',
+ 'rainy': 'RAIN',
+ 'cloudy': 'CLOUDS',
+ 'snowy': 'SNOW',
+ 'stormy': 'STORM',
+ 'foggy': 'FOG',
+ 'sunny': 'SUN',
+ 'night': 'NIGHT',
+ 'windy': 'WINDY',
+ 'not_available': 'NA',
+ 'unknown': 'UKN',
+}
+
+temp_conversions = {
+ 'C': lambda temp: temp,
+ 'F': lambda temp: (temp * 9 / 5) + 32,
+ 'K': lambda temp: temp + 273.15,
+}
+
+# Note: there are also unicode characters for units: ℃, ℉ and K
+temp_units = {
+ 'C': '°C',
+ 'F': '°F',
+ 'K': 'K',
+}
+
+
+class WeatherSegment(KwThreadedSegment):
+ interval = 600
+ default_location = None
+ location_urls = {}
+
+ @staticmethod
+ def key(location_query=None, **kwargs):
+ return location_query
+
+ def get_request_url(self, location_query):
+ try:
+ return self.location_urls[location_query]
+ except KeyError:
+ if location_query is None:
+ location_data = json.loads(urllib_read('http://geoip.nekudo.com/api/'))
+ location = ','.join((
+ location_data['city'],
+ location_data['country']['name'],
+ location_data['country']['code']
+ ))
+ self.info('Location returned by nekudo is {0}', location)
+ else:
+ location = location_query
+ query_data = {
+ 'q':
+ 'use "https://raw.githubusercontent.com/yql/yql-tables/master/weather/weather.bylocation.xml" as we;'
+ 'select * from weather.forecast where woeid in'
+ ' (select woeid from geo.places(1) where text="{0}") and u="c"'.format(location).encode('utf-8'),
+ 'format': 'json',
+ }
+ self.location_urls[location_query] = url = (
+ 'http://query.yahooapis.com/v1/public/yql?' + urllib_urlencode(query_data))
+ return url
+
+ def compute_state(self, location_query):
+ url = self.get_request_url(location_query)
+ raw_response = urllib_read(url)
+ if not raw_response:
+ self.error('Failed to get response')
+ return None
+
+ response = json.loads(raw_response)
+ try:
+ condition = response['query']['results']['channel']['item']['condition']
+ condition_code = int(condition['code'])
+ temp = float(condition['temp'])
+ except (KeyError, ValueError):
+ self.exception('Yahoo returned malformed or unexpected response: {0}', repr(raw_response))
+ return None
+
+ try:
+ icon_names = weather_conditions_codes[condition_code]
+ except IndexError:
+ if condition_code == 3200:
+ icon_names = ('not_available',)
+ self.warn('Weather is not available for location {0}', self.location)
+ else:
+ icon_names = ('unknown',)
+ self.error('Unknown condition code: {0}', condition_code)
+
+ return (temp, icon_names)
+
+ def render_one(self, weather, icons=None, unit='C', temp_format=None, temp_coldest=-30, temp_hottest=40, **kwargs):
+ if not weather:
+ return None
+
+ temp, icon_names = weather
+
+ for icon_name in icon_names:
+ if icons:
+ if icon_name in icons:
+ icon = icons[icon_name]
+ break
+ else:
+ icon = weather_conditions_icons[icon_names[-1]]
+
+ temp_format = temp_format or ('{temp:.0f}' + temp_units[unit])
+ converted_temp = temp_conversions[unit](temp)
+ if temp <= temp_coldest:
+ gradient_level = 0
+ elif temp >= temp_hottest:
+ gradient_level = 100
+ else:
+ gradient_level = (temp - temp_coldest) * 100.0 / (temp_hottest - temp_coldest)
+ groups = ['weather_condition_' + icon_name for icon_name in icon_names] + ['weather_conditions', 'weather']
+ return [
+ {
+ 'contents': icon + ' ',
+ 'highlight_groups': groups,
+ 'divider_highlight_group': 'background:divider',
+ },
+ {
+ 'contents': temp_format.format(temp=converted_temp),
+ 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'],
+ 'divider_highlight_group': 'background:divider',
+ 'gradient_level': gradient_level,
+ },
+ ]
+
+
+weather = with_docstring(WeatherSegment(),
+'''Return weather from Yahoo! Weather.
+
+Uses GeoIP lookup from http://geoip.nekudo.com to automatically determine
+your current location. This should be changed if you’re in a VPN or if your
+IP address is registered at another location.
+
+Returns a list of colorized icon and temperature segments depending on
+weather conditions.
+
+:param str unit:
+ temperature unit, can be one of ``F``, ``C`` or ``K``
+:param str location_query:
+ location query for your current location, e.g. ``oslo, norway``
+:param dict icons:
+ dict for overriding default icons, e.g. ``{'heavy_snow' : u'❆'}``
+:param str temp_format:
+ format string, receives ``temp`` as an argument. Should also hold unit.
+:param float temp_coldest:
+ coldest temperature. Any temperature below it will have gradient level equal
+ to zero.
+:param float temp_hottest:
+ hottest temperature. Any temperature above it will have gradient level equal
+ to 100. Temperatures between ``temp_coldest`` and ``temp_hottest`` receive
+ gradient level that indicates relative position in this interval
+ (``100 * (cur-coldest) / (hottest-coldest)``).
+
+Divider highlight group used: ``background:divider``.
+
+Highlight groups used: ``weather_conditions`` or ``weather``, ``weather_temp_gradient`` (gradient) or ``weather``.
+Also uses ``weather_conditions_{condition}`` for all weather conditions supported by Yahoo.
+''')