weather.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
  3. #
  4. # Permission is hereby granted, free of charge, to any person obtaining a copy
  5. # of this software and associated documentation files (the "Software"), to deal
  6. # in the Software without restriction, including without limitation the rights
  7. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. # copies of the Software, and to permit persons to whom the Software is
  9. # furnished to do so, subject to the following conditions:
  10. #
  11. # The above copyright notice and this permission notice shall be included in
  12. # all copies or substantial portions of the Software.
  13. #
  14. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. # THE SOFTWARE.
  21. #
  22. """
  23. pie_time.cards.weather
  24. ======================
  25. This module containse the WeatherCard class.
  26. """
  27. from threading import Timer
  28. import pygame
  29. import requests
  30. from pie_time.card import AbstractCard
  31. URL_TEMPLATE = (
  32. 'http://api.openweathermap.org/data/2.5/weather?q=%s&units=%s&APPID=%s'
  33. )
  34. class WeatherCard(AbstractCard):
  35. """
  36. The weather card.
  37. This cards displays the current weather for a selected city. The weather
  38. information is obtained from OpenWeatherMap.
  39. **Settings dictionary keys**:
  40. * **api_key** (*string*) - **required** API key.
  41. * **city** (*string*) - **required** name of the city.
  42. * **units** (*string*) - units name (``metric`` or ``imperial``). Defaults
  43. to :py:attr:`pie_time.cards.WeatherCard.UNITS`
  44. * **refresh_interval** (*int*) - refresh interval in seconds. Defaults to
  45. :py:attr:`pie_time.cards.WeatherCard.REFRESH_INTERVAL`
  46. * **city_color** (*tuple*) - city text color. Defaults to
  47. :py:attr:`pie_time.cards.WeatherCard.WHITE`
  48. * **icon_color** (*tuple*) - icon text color. Defaults to
  49. :py:attr:`pie_time.cards.WeatherCard.WHITE`
  50. * **temperature_color** (*tuple*) - temperature text color. Defaults to
  51. :py:attr:`pie_time.cards.WeatherCard.WHITE`
  52. * **conditions_color** (*tuple*) - conditions text color. Defaults to
  53. :py:attr:`pie_time.cards.WeatherCard.WHITE`
  54. """
  55. #: Default units
  56. UNITS = 'metric'
  57. #: Default refresh interval
  58. REFRESH_INTERVAL = 600
  59. #: White color for text
  60. WHITE = (255, 255, 255)
  61. WEATHER_CODE_TO_ICON = {
  62. '01d': u'',
  63. '01n': u'',
  64. '02d': u'',
  65. '02n': u'',
  66. '03d': u'',
  67. '03n': u'',
  68. '04d': u'',
  69. '04n': u'',
  70. '09d': u'',
  71. '09n': u'',
  72. '10d': u'',
  73. '10n': u'',
  74. '11d': u'',
  75. '11n': u'',
  76. '13d': u'',
  77. '13n': u'',
  78. '50d': u'',
  79. '50n': u''
  80. }
  81. ICON_SPACING = 24
  82. def initialize(self, refresh=True):
  83. assert 'api_key' in self._settings,\
  84. 'Configuration error: missing API key'
  85. assert 'city' in self._settings, 'Configuration error: missing city'
  86. self._text_font = pygame.font.Font(
  87. self.path_for_resource('opensans-light.ttf'), 30
  88. )
  89. self._temp_font = pygame.font.Font(
  90. self.path_for_resource('opensans-light.ttf'), 72
  91. )
  92. self._icon_font = pygame.font.Font(
  93. self.path_for_resource('linea-weather-10.ttf'), 128
  94. )
  95. self._timer = None
  96. self._current_conditions = None
  97. self._should_redraw = True
  98. if refresh:
  99. self._refresh_conditions()
  100. def _refresh_conditions(self):
  101. self._app.logger.debug('Refreshing conditions.')
  102. self._timer = None
  103. try:
  104. rsp = requests.get(
  105. URL_TEMPLATE % (
  106. self._settings['city'],
  107. self._settings.get('units', self.UNITS),
  108. self._settings['api_key']
  109. )
  110. )
  111. if rsp.status_code != 200:
  112. self._app.logger.error(
  113. 'WeatherCard: Received HTTP %d' % rsp.status_code
  114. )
  115. else:
  116. try:
  117. payload = rsp.json()
  118. self._current_conditions = {
  119. 'conditions': payload['weather'][0]['main'],
  120. 'icon': payload['weather'][0].get('icon', None),
  121. 'temperature': payload['main']['temp']
  122. }
  123. self._should_redraw = True
  124. except:
  125. self._app.logger.error(
  126. 'WeatherCard: ERROR!', exc_info=True
  127. )
  128. except:
  129. self._app.logger.error('WeatherCard: ERROR!', exc_info=True)
  130. self._timer = Timer(
  131. self._settings.get('refresh_interval', self.REFRESH_INTERVAL),
  132. self._refresh_conditions
  133. )
  134. self._timer.start()
  135. def _render_city(self):
  136. city_text = self._text_font.render(
  137. self._settings['city'], True,
  138. self._settings.get('city_color', self.WHITE)
  139. )
  140. return city_text
  141. def _render_conditions(self):
  142. conditions_text = self._text_font.render(
  143. self._current_conditions['conditions'].capitalize(),
  144. True, self._settings.get('conditions_color', self.WHITE)
  145. )
  146. return conditions_text
  147. def _render_icon(self):
  148. icon = self._current_conditions['icon']
  149. weather_icon = None
  150. if icon in self.WEATHER_CODE_TO_ICON:
  151. weather_icon = self._icon_font.render(
  152. self.WEATHER_CODE_TO_ICON[icon],
  153. True, self._settings.get('icon_color', self.WHITE)
  154. )
  155. return weather_icon
  156. def _render_temperature(self):
  157. temp_text = self._temp_font.render(
  158. u'%d°' % self._current_conditions['temperature'],
  159. True,
  160. self._settings.get('temperature_color', self.WHITE)
  161. )
  162. return temp_text
  163. def quit(self):
  164. if self._timer is not None:
  165. self._timer.cancel()
  166. def tick(self):
  167. if self._should_redraw:
  168. self.surface.fill(self.background_color)
  169. city_text = self._render_city()
  170. city_text_size = city_text.get_size()
  171. city_text_rect = (
  172. (self.width - city_text_size[0]) / 2.0,
  173. 0,
  174. city_text_size[0],
  175. city_text_size[1]
  176. )
  177. self.surface.blit(city_text, city_text_rect)
  178. if self._current_conditions:
  179. conditions_text = self._render_conditions()
  180. conditions_text_size = conditions_text.get_size()
  181. conditions_text_rect = (
  182. (self.width - conditions_text_size[0]) / 2.0,
  183. self.height - conditions_text_size[1],
  184. conditions_text_size[0],
  185. conditions_text_size[1]
  186. )
  187. self.surface.blit(conditions_text, conditions_text_rect)
  188. icon = self._render_icon()
  189. has_icon = (icon is not None)
  190. temp_text = self._render_temperature()
  191. temp_text_size = temp_text.get_size()
  192. if has_icon:
  193. icon_size = icon.get_size()
  194. icon_origin_x = (
  195. (
  196. self.width - (
  197. icon_size[0] + self.ICON_SPACING +
  198. temp_text_size[0]
  199. )
  200. ) / 2.0
  201. )
  202. icon_origin_y = (
  203. city_text_size[1] + (
  204. self.height - conditions_text_size[1] -
  205. city_text_size[1] - icon_size[1]
  206. ) / 2.0
  207. )
  208. icon_rect = (
  209. icon_origin_x,
  210. icon_origin_y,
  211. icon_size[0],
  212. icon_size[1]
  213. )
  214. self.surface.blit(icon, icon_rect)
  215. temp_text_origin_y = (
  216. city_text_size[1] + (
  217. self.height - conditions_text_size[1] -
  218. city_text_size[1] - temp_text_size[1]
  219. ) / 2.0
  220. )
  221. if has_icon:
  222. temp_text_origin_x = (
  223. icon_rect[0] + icon_size[0] +
  224. self.ICON_SPACING
  225. )
  226. temp_text_rect = (
  227. temp_text_origin_x,
  228. temp_text_origin_y,
  229. temp_text_size[0],
  230. temp_text_size[1]
  231. )
  232. else:
  233. temp_text_rect = (
  234. (self.width - temp_text_size[0]) / 2.0,
  235. temp_text_origin_y,
  236. temp_text_size[0],
  237. temp_text_size[1]
  238. )
  239. self.surface.blit(temp_text, temp_text_rect)
  240. self._should_redraw = False