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