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,
'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',
}
[docs]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,
"units": "metric"
}
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.
''')