summaryrefslogtreecommitdiffstats
path: root/powerline/segments/common/wthr.py
blob: 2c54cca997676fb834f1607647324649e645ef85 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)

import json
from collections import namedtuple

from powerline.lib.url import urllib_read, urllib_urlencode
from powerline.lib.threaded import KwThreadedSegment
from powerline.segments import with_docstring


_WeatherKey = namedtuple('Key', 'location_query weather_api_key')


# 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
# https://openweathermap.org/weather-conditions
weather_conditions_codes = {
	200: ('stormy',),
	201: ('stormy',),
	202: ('stormy',),
	210: ('stormy',),
	211: ('stormy',),
	212: ('stormy',),
	221: ('stormy',),
	230: ('stormy',),
	231: ('stormy',),
	232: ('stormy',),
	300: ('rainy',),
	301: ('rainy',),
	302: ('rainy',),
	310: ('rainy',),
	311: ('rainy',),
	312: ('rainy',),
	313: ('rainy',),
	314: ('rainy',),
	321: ('rainy',),
	500: ('rainy',),
	501: ('rainy',),
	502: ('rainy',),
	503: ('rainy',),
	504: ('rainy',),
	511: ('snowy',),
	520: ('rainy',),
	521: ('rainy',),
	522: ('rainy',),
	531: ('rainy',),
	600: ('snowy',),
	601: ('snowy',),
	602: ('snowy',),
	611: ('snowy',),
	612: ('snowy',),
	613: ('snowy',),
	615: ('snowy',),
	616: ('snowy',),
	620: ('snowy',),
	621: ('snowy',),
	622: ('snowy',),
	701: ('foggy',),
	711: ('foggy',),
	721: ('foggy',),
	731: ('foggy',),
	741: ('foggy',),
	751: ('foggy',),
	761: ('foggy',),
	762: ('foggy',),
	771: ('foggy',),
	781: ('foggy',),
	800: ('sunny',),
	801: ('cloudy',),
	802: ('cloudy',),
	803: ('cloudy',),
	804: ('cloudy',),
}

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 - 273.15,
	'F': lambda temp: (temp * 9 / 5) - 459.67,
	'K': lambda temp: temp,
}

# 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 = {}
	weather_api_key = "fbc9549d91a5e4b26c15be0dbdac3460"

	@staticmethod
	def key(location_query=None, **kwargs):
		try:
			weather_api_key = kwargs["weather_api_key"]
		except KeyError:
			weather_api_key = WeatherSegment.weather_api_key
		return _WeatherKey(location_query, weather_api_key)

	def get_request_url(self, weather_key):
		try:
			return self.location_urls[weather_key]
		except KeyError:
			query_data = {
				"appid": weather_key.weather_api_key
			}
			location_query = weather_key.location_query
			if location_query is None:
				location_data = json.loads(urllib_read('https://freegeoip.app/json/'))
				query_data["lat"] = location_data["latitude"]
				query_data["lon"] = location_data["longitude"]
			else:
				query_data["q"] = location_query
			self.location_urls[location_query] = url = (
				"https://api.openweathermap.org/data/2.5/weather?" +
				urllib_urlencode(query_data))
			return url

	def compute_state(self, weather_key):
		url = self.get_request_url(weather_key)
		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['weather'][0]
			condition_code = int(condition['id'])
			temp = float(response['main']['temp'])
		except (KeyError, ValueError):
			self.exception('OpenWeatherMap returned malformed or unexpected response: {0}', repr(raw_response))
			return None

		try:
			icon_names = weather_conditions_codes[condition_code]
		except IndexError:
			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 converted_temp <= temp_coldest:
			gradient_level = 0
		elif converted_temp >= temp_hottest:
			gradient_level = 100
		else:
			gradient_level = (converted_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 OpenWeatherMaps.

Uses GeoIP lookup from https://freegeoip.app 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 OpenWeatherMap.
''')