1# vim:fileencoding=utf-8:noet
2from __future__ import (unicode_literals, division, absolute_import, print_function)
3
4import json
5from collections import namedtuple
6
7from powerline.lib.url import urllib_read, urllib_urlencode
8from powerline.lib.threaded import KwThreadedSegment
9from powerline.segments import with_docstring
10
11
12_WeatherKey = namedtuple('Key', 'location_query weather_api_key')
13
14
15# XXX Warning: module name must not be equal to the segment name as long as this
16# segment is imported into powerline.segments.common module.
17
18
19# Weather condition code descriptions available at
20# https://openweathermap.org/weather-conditions
21weather_conditions_codes = {
22	200: ('stormy',),
23	201: ('stormy',),
24	202: ('stormy',),
25	210: ('stormy',),
26	211: ('stormy',),
27	212: ('stormy',),
28	221: ('stormy',),
29	230: ('stormy',),
30	231: ('stormy',),
31	232: ('stormy',),
32	300: ('rainy',),
33	301: ('rainy',),
34	302: ('rainy',),
35	310: ('rainy',),
36	311: ('rainy',),
37	312: ('rainy',),
38	313: ('rainy',),
39	314: ('rainy',),
40	321: ('rainy',),
41	500: ('rainy',),
42	501: ('rainy',),
43	502: ('rainy',),
44	503: ('rainy',),
45	504: ('rainy',),
46	511: ('snowy',),
47	520: ('rainy',),
48	521: ('rainy',),
49	522: ('rainy',),
50	531: ('rainy',),
51	600: ('snowy',),
52	601: ('snowy',),
53	602: ('snowy',),
54	611: ('snowy',),
55	612: ('snowy',),
56	613: ('snowy',),
57	615: ('snowy',),
58	616: ('snowy',),
59	620: ('snowy',),
60	621: ('snowy',),
61	622: ('snowy',),
62	701: ('foggy',),
63	711: ('foggy',),
64	721: ('foggy',),
65	731: ('foggy',),
66	741: ('foggy',),
67	751: ('foggy',),
68	761: ('foggy',),
69	762: ('foggy',),
70	771: ('foggy',),
71	781: ('foggy',),
72	800: ('sunny',),
73	801: ('cloudy',),
74	802: ('cloudy',),
75	803: ('cloudy',),
76	804: ('cloudy',),
77}
78
79weather_conditions_icons = {
80	'day':           'DAY',
81	'blustery':      'WIND',
82	'rainy':         'RAIN',
83	'cloudy':        'CLOUDS',
84	'snowy':         'SNOW',
85	'stormy':        'STORM',
86	'foggy':         'FOG',
87	'sunny':         'SUN',
88	'night':         'NIGHT',
89	'windy':         'WINDY',
90	'not_available': 'NA',
91	'unknown':       'UKN',
92}
93
94temp_conversions = {
95	'C': lambda temp: temp - 273.15,
96	'F': lambda temp: (temp * 9 / 5) - 459.67,
97	'K': lambda temp: temp,
98}
99
100# Note: there are also unicode characters for units: ℃, ℉ and  K
101temp_units = {
102	'C': '°C',
103	'F': '°F',
104	'K': 'K',
105}
106
107
108class WeatherSegment(KwThreadedSegment):
109	interval = 600
110	default_location = None
111	location_urls = {}
112	weather_api_key = "fbc9549d91a5e4b26c15be0dbdac3460"
113
114	@staticmethod
115	def key(location_query=None, **kwargs):
116		try:
117			weather_api_key = kwargs["weather_api_key"]
118		except KeyError:
119			weather_api_key = WeatherSegment.weather_api_key
120		return _WeatherKey(location_query, weather_api_key)
121
122	def get_request_url(self, weather_key):
123		try:
124			return self.location_urls[weather_key]
125		except KeyError:
126			query_data = {
127				"appid": weather_key.weather_api_key
128			}
129			location_query = weather_key.location_query
130			if location_query is None:
131				location_data = json.loads(urllib_read('https://freegeoip.app/json/'))
132				query_data["lat"] = location_data["latitude"]
133				query_data["lon"] = location_data["longitude"]
134			else:
135				query_data["q"] = location_query
136			self.location_urls[location_query] = url = (
137				"https://api.openweathermap.org/data/2.5/weather?" +
138				urllib_urlencode(query_data))
139			return url
140
141	def compute_state(self, weather_key):
142		url = self.get_request_url(weather_key)
143		raw_response = urllib_read(url)
144		if not raw_response:
145			self.error('Failed to get response')
146			return None
147
148		response = json.loads(raw_response)
149		try:
150			condition = response['weather'][0]
151			condition_code = int(condition['id'])
152			temp = float(response['main']['temp'])
153		except (KeyError, ValueError):
154			self.exception('OpenWeatherMap returned malformed or unexpected response: {0}', repr(raw_response))
155			return None
156
157		try:
158			icon_names = weather_conditions_codes[condition_code]
159		except IndexError:
160			icon_names = ('unknown',)
161			self.error('Unknown condition code: {0}', condition_code)
162
163		return (temp, icon_names)
164
165	def render_one(self, weather, icons=None, unit='C', temp_format=None, temp_coldest=-30, temp_hottest=40, **kwargs):
166		if not weather:
167			return None
168
169		temp, icon_names = weather
170
171		for icon_name in icon_names:
172			if icons:
173				if icon_name in icons:
174					icon = icons[icon_name]
175					break
176		else:
177			icon = weather_conditions_icons[icon_names[-1]]
178
179		temp_format = temp_format or ('{temp:.0f}' + temp_units[unit])
180		converted_temp = temp_conversions[unit](temp)
181		if converted_temp <= temp_coldest:
182			gradient_level = 0
183		elif converted_temp >= temp_hottest:
184			gradient_level = 100
185		else:
186			gradient_level = (converted_temp - temp_coldest) * 100.0 / (temp_hottest - temp_coldest)
187		groups = ['weather_condition_' + icon_name for icon_name in icon_names] + ['weather_conditions', 'weather']
188		return [
189			{
190				'contents': icon + ' ',
191				'highlight_groups': groups,
192				'divider_highlight_group': 'background:divider',
193			},
194			{
195				'contents': temp_format.format(temp=converted_temp),
196				'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'],
197				'divider_highlight_group': 'background:divider',
198				'gradient_level': gradient_level,
199			},
200		]
201
202
203weather = with_docstring(WeatherSegment(),
204'''Return weather from OpenWeatherMaps.
205
206Uses GeoIP lookup from https://freegeoip.app to automatically determine
207your current location. This should be changed if you’re in a VPN or if your
208IP address is registered at another location.
209
210Returns a list of colorized icon and temperature segments depending on
211weather conditions.
212
213:param str unit:
214	temperature unit, can be one of ``F``, ``C`` or ``K``
215:param str location_query:
216	location query for your current location, e.g. ``oslo, norway``
217:param dict icons:
218	dict for overriding default icons, e.g. ``{'heavy_snow' : u'❆'}``
219:param str temp_format:
220	format string, receives ``temp`` as an argument. Should also hold unit.
221:param float temp_coldest:
222	coldest temperature. Any temperature below it will have gradient level equal
223	to zero.
224:param float temp_hottest:
225	hottest temperature. Any temperature above it will have gradient level equal
226	to 100. Temperatures between ``temp_coldest`` and ``temp_hottest`` receive
227	gradient level that indicates relative position in this interval
228	(``100 * (cur-coldest) / (hottest-coldest)``).
229
230Divider highlight group used: ``background:divider``.
231
232Highlight groups used: ``weather_conditions`` or ``weather``, ``weather_temp_gradient`` (gradient) or ``weather``.
233Also uses ``weather_conditions_{condition}`` for all weather conditions supported by OpenWeatherMap.
234''')
235