Python RGB Matrix games and animations https://www.xythobuz.de/ledmatrix_v2.html
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

weather.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. #!/usr/bin/env python3
  2. # ----------------------------------------------------------------------------
  3. # "THE BEER-WARE LICENSE" (Revision 42):
  4. # <xythobuz@xythobuz.de> wrote this file. As long as you retain this notice
  5. # you can do whatever you want with this stuff. If we meet some day, and you
  6. # think this stuff is worth it, you can buy me a beer in return. Thomas Buck
  7. # ----------------------------------------------------------------------------
  8. import util
  9. from image import ImageScreen
  10. import time
  11. from datetime import datetime, timezone, timedelta
  12. from wetterdienst import Settings
  13. from wetterdienst.provider.dwd.mosmix import (
  14. DwdForecastDate,
  15. DwdMosmixRequest,
  16. DwdMosmixType,
  17. )
  18. import polars
  19. def get_first_capitalized_word(s):
  20. words = s.split()
  21. for w in words:
  22. if w[0].isupper():
  23. return w
  24. return None
  25. class WeatherScreen:
  26. def __init__(self, g, i, latlon, timestep = 5.0, refresh = (60.0 * 60.0)):
  27. self.gui = g
  28. self.input = i
  29. self.latlon = latlon
  30. self.timestep = timestep
  31. DrawText = util.getTextDrawer()
  32. self.t_head = DrawText(self.gui, (255, 255, 255), (0, 0, 0))
  33. self.t_sub = DrawText(self.gui, (0, 255, 255), (0, 0, 0))
  34. self.t_val = DrawText(self.gui, (255, 0, 255), (0, 0, 0))
  35. self.t_aux = DrawText(self.gui, (255, 255, 0), (0, 0, 0))
  36. self.params = [
  37. "weather_significant",
  38. "temperature_air_mean_200",
  39. "cloud_cover_effective",
  40. ]
  41. self.num_state = 4
  42. self.descriptions = [
  43. (95, 'leichtes oder mäßiges Gewitter mit Regen oder Schnee', 'weather_icon_95.png'),
  44. (57, 'mäßiger oder starker gefrierender Sprühregen', 'weather_icon_57.png'),
  45. (56, 'leichter gefrierender Sprühregen', 'weather_icon_56.png'),
  46. (67, 'mäßiger bis starker gefrierender Regen', 'weather_icon_67.png'),
  47. (66, 'leichter gefrierender Regen', 'weather_icon_66.png'),
  48. (86, 'mäßiger bis starker Schneeschauer', 'weather_icon_86.png'),
  49. (85, 'leichter Schneeschauer', 'weather_icon_85.png'),
  50. (84, 'mäßiger oder starker Schneeregenschauer', 'weather_icon_84.png'),
  51. (83, 'leichter Schneeregenschauer', 'weather_icon_83.png'),
  52. (82, 'äußerst heftiger Regenschauer', 'weather_icon_82.png'),
  53. (81, 'mäßiger oder starker Regenschauer', 'weather_icon_81.png'),
  54. (80, 'leichter Regenschauer', 'weather_icon_80.png'),
  55. (75, 'durchgehend starker Schneefall', 'weather_icon_75.png'),
  56. (73, 'durchgehend mäßiger Schneefall', 'weather_icon_73.png'),
  57. (71, 'durchgehend leichter Schneefall', 'weather_icon_71.png'),
  58. (69, 'mäßger oder starker Schneeregen', 'weather_icon_69.png'),
  59. (68, 'leichter Schneeregen', 'weather_icon_68.png'),
  60. (55, 'durchgehend starker Sprühregen', 'weather_icon_55.png'),
  61. (53, 'durchgehend mäßiger Sprühregen', 'weather_icon_53.png'),
  62. (51, 'durchgehend leichter Sprühregen', 'weather_icon_51.png'),
  63. (65, 'durchgehend starker Regen', 'weather_icon_65.png'),
  64. (63, 'durchgehend mäßiger Regen', 'weather_icon_63.png'),
  65. (61, 'durchgehend leichter Regen', 'weather_icon_61.png'),
  66. (49, 'Nebel mit Reifansatz, Himmel nicht erkennbar, unverändert', 'weather_icon_49.png'),
  67. (45, 'Nebel, Himmel nicht erkennbar', 'weather_icon_45.png'),
  68. (3, 'Bewölkung zunehmend', None),
  69. (2, 'Bewölkung unverändert', None),
  70. (1, 'Bewölkung abnehmend', None),
  71. (0, 'keine Bewölkungsentwicklung', None),
  72. ]
  73. self.find_station()
  74. self.restart()
  75. def restart(self):
  76. self.state = 0
  77. self.old_keys = self.input.empty() # TODO support missing input
  78. self.done = False
  79. self.last = time.time()
  80. def find_station(self):
  81. self.gui.loop_start()
  82. self.t_head.setText("Weather:", "lemon")
  83. self.t_head.draw(3, -self.gui.height / 2 + 7)
  84. self.t_sub.setText("Loading...", "tom-thumb")
  85. self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 4)
  86. self.t_val.setText("Station...", "tom-thumb")
  87. self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 6)
  88. self.gui.loop_end()
  89. settings = Settings(ts_shape=True, ts_humanize=True, cache_disable=False)
  90. request = DwdMosmixRequest(
  91. parameter=self.params,
  92. start_issue=DwdForecastDate.LATEST,
  93. mosmix_type=DwdMosmixType.LARGE,
  94. settings=settings,
  95. )
  96. stations = request.filter_by_distance(latlon=self.latlon, distance=30)
  97. self.station = stations.df[0]
  98. print("Found station '{}' ({}) in {:.2f} km distance".format(
  99. self.station["name"][0],
  100. self.station["station_id"][0],
  101. self.station["distance"][0]
  102. ))
  103. self.forecast = request.filter_by_station_id(station_id=[
  104. self.station["station_id"][0],
  105. ])
  106. self.parse_forecast()
  107. def get_forecast(self):
  108. self.gui.loop_start()
  109. self.t_head.setText("Weather:", "lemon")
  110. self.t_head.draw(3, -self.gui.height / 2 + 7)
  111. self.t_sub.setText("Loading...", "tom-thumb")
  112. self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 4)
  113. self.t_val.setText("Refresh...", "tom-thumb")
  114. self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 6)
  115. self.gui.loop_end()
  116. settings = Settings(ts_shape=True, ts_humanize=True, cache_disable=False)
  117. request = DwdMosmixRequest(
  118. parameter=self.params,
  119. start_issue=DwdForecastDate.LATEST,
  120. mosmix_type=DwdMosmixType.LARGE,
  121. settings=settings,
  122. )
  123. self.forecast = request.filter_by_station_id(station_id=[self.station["station_id"][0]])
  124. self.parse_forecast()
  125. def parse_forecast(self):
  126. self.gui.loop_start()
  127. self.t_head.setText("Weather:", "lemon")
  128. self.t_head.draw(3, -self.gui.height / 2 + 7)
  129. self.t_sub.setText("Parsing", "tom-thumb")
  130. self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 4)
  131. self.t_val.setText("Forecast", "tom-thumb")
  132. self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 6)
  133. self.gui.loop_end()
  134. self.last_forecast = time.time()
  135. response = next(self.forecast.values.query())
  136. response_subset = response.df.select(["date", "parameter", "value"])
  137. # only datapoints from -1h to +12h from now
  138. start_time = datetime.now(timezone.utc) - timedelta(hours=1)
  139. end_time = datetime.now(timezone.utc) + timedelta(hours=12)
  140. self.data = response_subset.filter((polars.col("date") >= start_time) & (polars.col("date") <= end_time))
  141. #print(self.data)
  142. #for p in self.params:
  143. # print(self.data.filter(polars.col("parameter") == p)[0])
  144. #for i in range(0, len(self.data)):
  145. # print(self.data[i])
  146. def buttons(self):
  147. keys = self.input.get()
  148. if keys["up"] and (not self.old_keys["up"]) and (not self.old_keys["select"]):
  149. self.state = (self.state + 1) % self.num_state
  150. self.last = time.time()
  151. elif keys["down"] and (not self.old_keys["down"]) and (not self.old_keys["select"]):
  152. self.state = (self.state - 1) % self.num_state
  153. self.last = time.time()
  154. self.old_keys = keys.copy()
  155. def finished(self):
  156. return self.done
  157. def draw_station_info(self):
  158. # heading
  159. self.t_head.setText("Weather:", "lemon")
  160. self.t_head.draw(3, -self.gui.height / 2 + 7)
  161. # station info (2 lines)
  162. self.t_sub.setText(self.station["name"][0], "tom-thumb")
  163. self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 2)
  164. self.t_sub.draw(self.gui.width, -self.gui.height / 2 + 5 + 6 * 3)
  165. # distance
  166. self.t_val.setText("Distance:", "tom-thumb")
  167. self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 5)
  168. self.t_val.setText("{:.2f} km".format(self.station["distance"][0]), "tom-thumb")
  169. self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 6)
  170. # lat lon
  171. self.t_aux.setText("Lat: {:.2f}".format(self.station["latitude"][0]), "tom-thumb")
  172. self.t_aux.draw(0, -self.gui.height / 2 + 5 + 6 * 8)
  173. self.t_aux.setText("Lon: {:.2f}".format(self.station["longitude"][0]), "tom-thumb")
  174. self.t_aux.draw(0, -self.gui.height / 2 + 5 + 6 * 9)
  175. def draw_weather(self):
  176. # heading
  177. self.t_head.setText("Weather:", "lemon")
  178. self.t_head.draw(3, -self.gui.height / 2 + 7)
  179. # convert weather id
  180. val = self.data.filter(polars.col("parameter") == self.params[0])[0]["value"][0]
  181. name = "ID {} Unknown".format(val)
  182. img = None
  183. for i, d in enumerate(self.descriptions):
  184. if d[0] != val:
  185. continue
  186. if d[2] == None:
  187. self.descriptions[i] = (d[0], d[1], "weather_unknown.png")
  188. if isinstance(d[2], str):
  189. w = 50
  190. target_size = (w, w)
  191. iscr = ImageScreen(self.gui, d[2], 0.2, 1, 5.0, None, target_size, False)
  192. self.descriptions[i] = (d[0], d[1], iscr)
  193. iscr.xOff = (self.gui.width - w) / 2
  194. iscr.yOff = self.gui.height - w + 5
  195. img = iscr
  196. else:
  197. img = d[2]
  198. name = get_first_capitalized_word(d[1])
  199. # print weather description text
  200. self.t_sub.setText(name, "tom-thumb")
  201. self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 1 + 2)
  202. #self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 2)
  203. #self.t_sub.draw(self.gui.width, -self.gui.height / 2 + 5 + 6 * 3)
  204. #self.t_sub.draw(self.gui.width * 2, -self.gui.height / 2 + 5 + 6 * 4)
  205. # show image if it exists
  206. if img != None:
  207. img.draw()
  208. # id of weather / image
  209. self.t_aux.setText("{}".format(int(val)), "tom-thumb")
  210. self.t_aux.draw(0, -self.gui.height / 2 + 5 + 6 * 9 + 3)
  211. def draw_temperature(self):
  212. # heading
  213. self.t_head.setText("Temps:", "lemon")
  214. self.t_head.draw(3, -self.gui.height / 2 + 7)
  215. self.t_sub.setText("Current:", "lemon")
  216. self.t_sub.draw(3, -self.gui.height / 2 + 20)
  217. # current temperature
  218. val = self.data.filter(polars.col("parameter") == self.params[1])[0]["value"][0]
  219. val = val - 273.15 # kelvin to celsius
  220. self.t_val.setText("{:.1f}°C".format(val), "tom-thumb")
  221. self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 5)
  222. def draw_cloud_cover(self):
  223. # heading
  224. self.t_head.setText("Cloud", "lemon")
  225. self.t_head.draw(3, -self.gui.height / 2 + 7)
  226. self.t_sub.setText("Coverage:", "lemon")
  227. self.t_sub.draw(3, -self.gui.height / 2 + 20)
  228. val = self.data.filter(polars.col("parameter") == self.params[2])[0]["value"][0]
  229. self.t_val.setText("{:.1f}%".format(val), "tom-thumb")
  230. self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 5)
  231. def draw(self):
  232. # handle button input
  233. if self.input != None:
  234. self.buttons()
  235. # draw screen contents
  236. if self.state == 0:
  237. self.draw_station_info()
  238. elif self.state == 1:
  239. self.draw_weather()
  240. elif self.state == 2:
  241. self.draw_temperature()
  242. elif self.state == 3:
  243. self.draw_cloud_cover()
  244. # advance to next screen after time has passed
  245. if (time.time() - self.last) >= self.timestep:
  246. self.state = (self.state + 1) % self.num_state
  247. if self.state == 0:
  248. self.done = True
  249. self.last = time.time()
  250. # draw progress bar on bottom most row
  251. elapsed = (time.time() - self.last)
  252. ratio = elapsed / self.timestep
  253. for i in range(0, int(ratio * self.gui.width) + 1):
  254. self.gui.set_pixel(i, self.gui.height - 1, (0, 255, 0))
  255. if __name__ == "__main__":
  256. from config import Config
  257. i = util.getInput()
  258. t = util.getTarget(i)
  259. s = WeatherScreen(t, i, Config.weather_latlon, 5.0, 60.0 * 10.0)
  260. util.loop(t, s.draw)