瀏覽代碼

more work on weather widget

Thomas Buck 8 月之前
父節點
當前提交
97a4fa0a81

+ 2
- 0
README.md 查看文件

@@ -55,3 +55,5 @@ The fonts from [cmvnd/fonts](https://github.com/cmvnd/fonts) are licensed as GPL
55 55
 The tiny font is from [robey](https://robey.lag.net/2010/01/23/tiny-monospace-font.html) and licensed as CC0.
56 56
 
57 57
 The included GIFs are from [GifCities](https://gifcities.org/?q=32).
58
+
59
+The included DWD weather API documentation is from [their website](https://www.dwd.de/DE/leistungen/opendata/hilfe.html).

+ 19
- 12
image.py 查看文件

@@ -25,7 +25,7 @@ from PIL import GifImagePlugin
25 25
 GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
26 26
 
27 27
 class ImageScreen:
28
-    def __init__(self, g, p, t = 0.2, i = 1, to = 5.0, bg = None):
28
+    def __init__(self, g, p, t = 0.2, i = 1, to = 5.0, bg = None, target_size = None, nearest = True):
29 29
         self.gui = g
30 30
         self.time = t
31 31
         self.iterations = i
@@ -41,32 +41,38 @@ class ImageScreen:
41 41
             self.image.is_animated = False
42 42
             self.image.n_frames = 1
43 43
 
44
+        if target_size == None:
45
+            target_size = (self.gui.width, self.gui.height)
46
+
44 47
         # automatically crop and scale large images
45
-        if not self.image.is_animated and ((self.image.width > self.gui.width) or (self.image.height > self.gui.height)):
48
+        if not self.image.is_animated and ((self.image.width > target_size[0]) or (self.image.height > target_size[1])):
46 49
             # crop to visible area
47 50
             self.image = self.image.crop(self.image.getbbox())
48 51
 
49 52
             # keep the aspect ratio and fit within visible area
50 53
             ratio = self.image.width / self.image.height
51
-            max_width = int(ratio * self.gui.height)
52
-            max_height = int(self.gui.width / ratio)
53
-            if (max_height >= self.gui.height) or (((self.gui.width - max_width) < (self.gui.height - max_height)) and ((self.gui.width - max_width) >= 0)):
54
+            max_width = int(ratio * target_size[1])
55
+            max_height = int(target_size[0] / ratio)
56
+            if (max_height >= target_size[1]) or (((target_size[0] - max_width) < (target_size[1] - max_height)) and ((target_size[0] - max_width) >= 0)):
54 57
                 width = max_width
55
-                height = self.gui.height
58
+                height = target_size[1]
56 59
             else:
57
-                width = self.gui.width
60
+                width = target_size[0]
58 61
                 height = max_height
59 62
 
60 63
             # resize
61
-            self.image = self.image.resize((width, height),
62
-                                           Image.Resampling.NEAREST)
64
+            if nearest:
65
+                self.image = self.image.resize((width, height),
66
+                                            Image.Resampling.NEAREST)
67
+            else:
68
+                self.image = self.image.resize((width, height))
63 69
 
64 70
             # new image object is also missing these
65 71
             self.image.is_animated = False
66 72
             self.image.n_frames = 1
67 73
 
68 74
         # enlarge small images
69
-        if not self.image.is_animated and ((self.image.width * 2) <= self.gui.width) and ((self.image.height * 2) <= self.gui.height):
75
+        if not self.image.is_animated and ((self.image.width * 2) <= target_size[0]) and ((self.image.height * 2) <= target_size[1]):
70 76
             self.image = self.image.crop(self.image.getbbox())
71 77
             self.image = self.image.resize((self.image.width * 2, self.image.height * 2),
72 78
                                            Image.Resampling.NEAREST)
@@ -133,10 +139,11 @@ if __name__ == "__main__":
133 139
     import sys
134 140
 
135 141
     import util
136
-    t = util.getTarget()
142
+    i = util.getInput()
143
+    t = util.getTarget(i)
137 144
 
138 145
     from manager import Manager
139
-    m = Manager(t)
146
+    m = Manager(t, i)
140 147
 
141 148
     scriptDir = os.path.dirname(os.path.realpath(__file__))
142 149
     imageDir = os.path.join(scriptDir, "images")

二進制
images/weather_icon_45.png 查看文件


二進制
images/weather_icon_49.png 查看文件


二進制
images/weather_icon_51.png 查看文件


二進制
images/weather_icon_53.png 查看文件


二進制
images/weather_icon_55.png 查看文件


二進制
images/weather_icon_56.png 查看文件


二進制
images/weather_icon_57.png 查看文件


二進制
images/weather_icon_61.png 查看文件


二進制
images/weather_icon_63.png 查看文件


二進制
images/weather_icon_65.png 查看文件


二進制
images/weather_icon_66.png 查看文件


二進制
images/weather_icon_67.png 查看文件


二進制
images/weather_icon_68.png 查看文件


二進制
images/weather_icon_69.png 查看文件


二進制
images/weather_icon_71.png 查看文件


二進制
images/weather_icon_73.png 查看文件


二進制
images/weather_icon_75.png 查看文件


二進制
images/weather_icon_80.png 查看文件


二進制
images/weather_icon_81.png 查看文件


二進制
images/weather_icon_82.png 查看文件


二進制
images/weather_icon_83.png 查看文件


二進制
images/weather_icon_84.png 查看文件


二進制
images/weather_icon_85.png 查看文件


二進制
images/weather_icon_86.png 查看文件


二進制
images/weather_icon_95.png 查看文件


+ 67
- 0
livingroom.py 查看文件

@@ -0,0 +1,67 @@
1
+#!/usr/bin/env python3
2
+
3
+# ----------------------------------------------------------------------------
4
+# "THE BEER-WARE LICENSE" (Revision 42):
5
+# <xythobuz@xythobuz.de> wrote this file.  As long as you retain this notice
6
+# you can do whatever you want with this stuff. If we meet some day, and you
7
+# think this stuff is worth it, you can buy me a beer in return.   Thomas Buck
8
+# ----------------------------------------------------------------------------
9
+
10
+camp_pink = (251, 72, 196)
11
+camp_green = (63, 255, 33)
12
+
13
+from splash import SplashScreen
14
+from scroll import ScrollText
15
+from solid import Solid
16
+from life import GameOfLife
17
+from net import CheckHTTP
18
+from image import ImageScreen
19
+from qr import QRScreen
20
+from snake import Snake
21
+from gamepad import InputWrapper
22
+from manager import Manager
23
+from tetris import Tetris
24
+from breakout import Breakout
25
+from weather import WeatherScreen
26
+from config import Config
27
+import util
28
+
29
+# Need to import InputWrapper before initializing RGB Matrix on Pi
30
+i = util.getInput()
31
+t = util.getTarget(i)
32
+
33
+# Loading fonts and graphics takes a while.
34
+# So show a splash screen while the user waits.
35
+splash = SplashScreen(t)
36
+t.loop_start()
37
+splash.draw()
38
+t.loop_end()
39
+
40
+m = Manager(t, i)
41
+
42
+m.add(ImageScreen(t, "cann.png"))
43
+m.add(Solid(t, 1.0))
44
+
45
+m.add(WeatherScreen(t, i, Config.weather_latlon))
46
+m.add(Solid(t, 1.0))
47
+
48
+m.add(GameOfLife(t, 20, (0, 255, 0), (0, 0, 0), None, 2.0))
49
+m.add(Solid(t, 1.0))
50
+
51
+m.add(ImageScreen(t, "cann2.png"))
52
+m.add(Solid(t, 1.0))
53
+
54
+m.add(ImageScreen(t, "earth.gif"))
55
+m.add(Solid(t, 1.0))
56
+
57
+#m.add(Breakout(t, i))
58
+#m.add(Solid(t, 1.0))
59
+
60
+#m.add(Tetris(t, i,))
61
+#m.add(Solid(t, 1.0))
62
+
63
+#m.add(Snake(t, i, camp_pink, camp_green))
64
+#m.add(Solid(t, 1.0))
65
+
66
+m.restart()
67
+util.loop(t, m.draw)

+ 60
- 0
tools/convert.py 查看文件

@@ -0,0 +1,60 @@
1
+#!/usr/bin/env python3
2
+
3
+# Requires openpyxl and openpyxl_image_loader.
4
+#
5
+# ----------------------------------------------------------------------------
6
+# "THE BEER-WARE LICENSE" (Revision 42):
7
+# <xythobuz@xythobuz.de> wrote this file.  As long as you retain this notice
8
+# you can do whatever you want with this stuff. If we meet some day, and you
9
+# think this stuff is worth it, you can buy me a beer in return.   Thomas Buck
10
+# ----------------------------------------------------------------------------
11
+
12
+import sys
13
+from openpyxl import load_workbook
14
+from openpyxl_image_loader import SheetImageLoader
15
+
16
+if __name__ != "__main__":
17
+    raise RuntimeError("Please run this file directly")
18
+
19
+if len(sys.argv) < 2:
20
+    raise RuntimeError("Please pass input file as argument")
21
+
22
+wb = load_workbook(filename = sys.argv[1])
23
+ws = wb.active
24
+image_loader = SheetImageLoader(ws)
25
+
26
+data = []
27
+
28
+for row in ws:
29
+    try:
30
+        id = int(row[0].value)
31
+    except:
32
+        continue
33
+
34
+    text = str(row[1].value)
35
+    #print("id={} text='{}'".format(id, text))
36
+
37
+    if not image_loader.image_in(row[2].coordinate):
38
+        print("No image found for ID {}".format(id))
39
+        image = None
40
+    else:
41
+        image = image_loader.get(row[2].coordinate)
42
+
43
+    data.append((
44
+        id, text, image
45
+    ))
46
+
47
+print("Found {} IDs:".format(len(data)))
48
+print()
49
+
50
+print("    self.descriptions = [")
51
+for d in data:
52
+    if d[2] != None:
53
+        img_name = "weather_icon_{}.png".format(d[0])
54
+        d[2].save(img_name)
55
+        img_name = "'" + img_name + "'"
56
+    else:
57
+        img_name = "None"
58
+    print("        ({}, '{}', {}),".format(d[0], d[1], img_name))
59
+print("    ]")
60
+print()

二進制
tools/mosmix_element_weather_xls.xlsx 查看文件


+ 116
- 11
weather.py 查看文件

@@ -8,6 +8,8 @@
8 8
 # ----------------------------------------------------------------------------
9 9
 
10 10
 import util
11
+from image import ImageScreen
12
+
11 13
 import time
12 14
 from datetime import datetime, timezone, timedelta
13 15
 
@@ -19,6 +21,13 @@ from wetterdienst.provider.dwd.mosmix import (
19 21
 )
20 22
 import polars
21 23
 
24
+def get_first_capitalized_word(s):
25
+    words = s.split()
26
+    for w in words:
27
+        if w[0].isupper():
28
+            return w
29
+    return None
30
+
22 31
 class WeatherScreen:
23 32
     def __init__(self, g, i, latlon, timestep = 5.0, refresh = (60.0 * 60.0)):
24 33
         self.gui = g
@@ -37,7 +46,39 @@ class WeatherScreen:
37 46
             "temperature_air_mean_200",
38 47
             "cloud_cover_effective",
39 48
         ]
40
-        self.num_state = len(self.params) + 1
49
+        self.num_state = 4
50
+
51
+        self.descriptions = [
52
+            (95, 'leichtes oder mäßiges Gewitter mit Regen oder Schnee', 'weather_icon_95.png'),
53
+            (57, 'mäßiger oder starker gefrierender Sprühregen', 'weather_icon_57.png'),
54
+            (56, 'leichter gefrierender Sprühregen', 'weather_icon_56.png'),
55
+            (67, 'mäßiger bis starker gefrierender Regen', 'weather_icon_67.png'),
56
+            (66, 'leichter gefrierender Regen', 'weather_icon_66.png'),
57
+            (86, 'mäßiger bis starker Schneeschauer', 'weather_icon_86.png'),
58
+            (85, 'leichter Schneeschauer', 'weather_icon_85.png'),
59
+            (84, 'mäßiger oder starker Schneeregenschauer', 'weather_icon_84.png'),
60
+            (83, 'leichter Schneeregenschauer', 'weather_icon_83.png'),
61
+            (82, 'äußerst heftiger Regenschauer', 'weather_icon_82.png'),
62
+            (81, 'mäßiger oder starker Regenschauer', 'weather_icon_81.png'),
63
+            (80, 'leichter Regenschauer', 'weather_icon_80.png'),
64
+            (75, 'durchgehend starker Schneefall', 'weather_icon_75.png'),
65
+            (73, 'durchgehend mäßiger Schneefall', 'weather_icon_73.png'),
66
+            (71, 'durchgehend leichter Schneefall', 'weather_icon_71.png'),
67
+            (69, 'mäßger oder starker Schneeregen', 'weather_icon_69.png'),
68
+            (68, 'leichter Schneeregen', 'weather_icon_68.png'),
69
+            (55, 'durchgehend starker Sprühregen', 'weather_icon_55.png'),
70
+            (53, 'durchgehend mäßiger Sprühregen', 'weather_icon_53.png'),
71
+            (51, 'durchgehend leichter Sprühregen', 'weather_icon_51.png'),
72
+            (65, 'durchgehend starker Regen', 'weather_icon_65.png'),
73
+            (63, 'durchgehend mäßiger Regen', 'weather_icon_63.png'),
74
+            (61, 'durchgehend leichter Regen', 'weather_icon_61.png'),
75
+            (49, 'Nebel mit Reifansatz, Himmel nicht erkennbar, unverändert', 'weather_icon_49.png'),
76
+            (45, 'Nebel, Himmel nicht erkennbar', 'weather_icon_45.png'),
77
+            (3, 'Bewölkung zunehmend', None),
78
+            (2, 'Bewölkung unverändert', None),
79
+            (1, 'Bewölkung abnehmend', None),
80
+            (0, 'keine Bewölkungsentwicklung', None),
81
+        ]
41 82
 
42 83
         self.find_station()
43 84
         self.restart()
@@ -58,10 +99,20 @@ class WeatherScreen:
58 99
             "start": False,
59 100
             "select": False,
60 101
         }
102
+        self.done = False
61 103
         self.last = time.time()
62 104
 
63 105
     def find_station(self):
64
-        settings = Settings(ts_shape=True, ts_humanize=True)
106
+        self.gui.loop_start()
107
+        self.t_head.setText("Weather:", "lemon")
108
+        self.t_head.draw(3, -self.gui.height / 2 + 7)
109
+        self.t_sub.setText("Loading...", "tom-thumb")
110
+        self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 4)
111
+        self.t_val.setText("Station...", "tom-thumb")
112
+        self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 6)
113
+        self.gui.loop_end()
114
+
115
+        settings = Settings(ts_shape=True, ts_humanize=True, cache_disable=False)
65 116
         request = DwdMosmixRequest(
66 117
             parameter=self.params,
67 118
             start_issue=DwdForecastDate.LATEST,
@@ -83,7 +134,16 @@ class WeatherScreen:
83 134
         self.parse_forecast()
84 135
 
85 136
     def get_forecast(self):
86
-        settings = Settings(ts_shape=True, ts_humanize=True)
137
+        self.gui.loop_start()
138
+        self.t_head.setText("Weather:", "lemon")
139
+        self.t_head.draw(3, -self.gui.height / 2 + 7)
140
+        self.t_sub.setText("Loading...", "tom-thumb")
141
+        self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 4)
142
+        self.t_val.setText("Refresh...", "tom-thumb")
143
+        self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 6)
144
+        self.gui.loop_end()
145
+
146
+        settings = Settings(ts_shape=True, ts_humanize=True, cache_disable=False)
87 147
         request = DwdMosmixRequest(
88 148
             parameter=self.params,
89 149
             start_issue=DwdForecastDate.LATEST,
@@ -94,6 +154,15 @@ class WeatherScreen:
94 154
         self.parse_forecast()
95 155
 
96 156
     def parse_forecast(self):
157
+        self.gui.loop_start()
158
+        self.t_head.setText("Weather:", "lemon")
159
+        self.t_head.draw(3, -self.gui.height / 2 + 7)
160
+        self.t_sub.setText("Parsing", "tom-thumb")
161
+        self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 4)
162
+        self.t_val.setText("Forecast", "tom-thumb")
163
+        self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 6)
164
+        self.gui.loop_end()
165
+
97 166
         self.last_forecast = time.time()
98 167
 
99 168
         response = next(self.forecast.values.query())
@@ -116,14 +185,14 @@ class WeatherScreen:
116 185
         if keys["up"] and (not self.old_keys["up"]) and (not self.old_keys["select"]):
117 186
             self.state = (self.state + 1) % self.num_state
118 187
             self.last = time.time()
119
-        elif keys["down"] and (not self.old_keys["select"]):
188
+        elif keys["down"] and (not self.old_keys["down"]) and (not self.old_keys["select"]):
120 189
             self.state = (self.state - 1) % self.num_state
121 190
             self.last = time.time()
122 191
 
123 192
         self.old_keys = keys.copy()
124 193
 
125 194
     def finished(self):
126
-        return False # TODO
195
+        return self.done
127 196
 
128 197
     def draw_station_info(self):
129 198
         # heading
@@ -152,9 +221,41 @@ class WeatherScreen:
152 221
         self.t_head.setText("Weather:", "lemon")
153 222
         self.t_head.draw(3, -self.gui.height / 2 + 7)
154 223
 
224
+        # convert weather id
155 225
         val = self.data.filter(polars.col("parameter") == self.params[0])[0]["value"][0]
156
-        self.t_val.setText("{:.0f}".format(val), "tom-thumb")
157
-        self.t_val.draw(0, -self.gui.height / 2 + 5 + 6 * 5)
226
+        name = "ID {} Unknown".format(val)
227
+        img = None
228
+        for i, d in enumerate(self.descriptions):
229
+            if d[0] != val:
230
+                continue
231
+
232
+            if isinstance(d[2], str):
233
+                w = 50
234
+                target_size = (w, w)
235
+                iscr = ImageScreen(self.gui, d[2], 0.2, 1, 5.0, None, target_size, False)
236
+                self.descriptions[i] = (d[0], d[1], iscr)
237
+                iscr.xOff = (self.gui.width - w) / 2
238
+                iscr.yOff = self.gui.height - w + 5
239
+                img = iscr
240
+            else:
241
+                img = d[2]
242
+
243
+            name = get_first_capitalized_word(d[1])
244
+
245
+        # print weather description text
246
+        self.t_sub.setText(name, "tom-thumb")
247
+        self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 1 + 2)
248
+        #self.t_sub.draw(0, -self.gui.height / 2 + 5 + 6 * 2)
249
+        #self.t_sub.draw(self.gui.width, -self.gui.height / 2 + 5 + 6 * 3)
250
+        #self.t_sub.draw(self.gui.width * 2, -self.gui.height / 2 + 5 + 6 * 4)
251
+
252
+        # show image if it exists
253
+        if img != None:
254
+            img.draw()
255
+
256
+        # id of weather / image
257
+        self.t_aux.setText("{}".format(int(val)), "tom-thumb")
258
+        self.t_aux.draw(0, -self.gui.height / 2 + 5 + 6 * 9 + 3)
158 259
 
159 260
     def draw_temperature(self):
160 261
         # heading
@@ -171,8 +272,10 @@ class WeatherScreen:
171 272
 
172 273
     def draw_cloud_cover(self):
173 274
         # heading
174
-        self.t_head.setText("Clouds:", "lemon")
275
+        self.t_head.setText("Cloud", "lemon")
175 276
         self.t_head.draw(3, -self.gui.height / 2 + 7)
277
+        self.t_sub.setText("Coverage:", "lemon")
278
+        self.t_sub.draw(3, -self.gui.height / 2 + 20)
176 279
 
177 280
         val = self.data.filter(polars.col("parameter") == self.params[2])[0]["value"][0]
178 281
         self.t_val.setText("{:.1f} %".format(val), "tom-thumb")
@@ -196,13 +299,15 @@ class WeatherScreen:
196 299
         # advance to next screen after time has passed
197 300
         if (time.time() - self.last) >= self.timestep:
198 301
             self.state = (self.state + 1) % self.num_state
302
+            if self.state == 0:
303
+                self.done = True
199 304
             self.last = time.time()
200 305
 
201 306
         # draw progress bar on bottom most row
202 307
         elapsed = (time.time() - self.last)
203 308
         ratio = elapsed / self.timestep
204
-        for i in range(0, int(ratio * self.gui.width)):
205
-            self.gui.set_pixel(i, self.gui.height - 1, (255, 0, 0))
309
+        for i in range(0, int(ratio * self.gui.width) + 1):
310
+            self.gui.set_pixel(i, self.gui.height - 1, (0, 255, 0))
206 311
 
207 312
 if __name__ == "__main__":
208 313
     from config import Config
@@ -210,5 +315,5 @@ if __name__ == "__main__":
210 315
     i = util.getInput()
211 316
     t = util.getTarget(i)
212 317
 
213
-    s = WeatherScreen(t, i, Config.weather_latlon, 2.0, 60.0 * 10.0)
318
+    s = WeatherScreen(t, i, Config.weather_latlon, 5.0, 60.0 * 10.0)
214 319
     util.loop(t, s.draw)

Loading…
取消
儲存