Skip to content

Commit 5ded4b4

Browse files
authored
weather: add forecasts, switch to download manager (#75)
* weather: add forecasts, and summarize them * weather: Switch to download manager We had open coded http protocol, this simplifies it and switches to https
1 parent 0ac9d71 commit 5ded4b4

1 file changed

Lines changed: 260 additions & 71 deletions

File tree

  • internal_filesystem/apps/cz.ucw.pavel.weather/assets

internal_filesystem/apps/cz.ucw.pavel.weather/assets/main.py

Lines changed: 260 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
except ImportError:
1313
pass
1414

15-
from mpos import Activity, MposKeyboard
15+
from mpos import Activity, MposKeyboard, DownloadManager
1616

1717
import ujson
1818
import utime
@@ -55,22 +55,87 @@ class WData:
5555
99: "Thunderstorm + hail",
5656
}
5757

58+
def init(self):
59+
pass
60+
5861
def code_to_text(self, code):
5962
return self.WMO_CODES.get(int(code), "Unknown")
6063

61-
class Hourly(WData):
62-
def __init__(self, cw):
63-
self.temp = cw["temperature_2m"]
64-
self.wind = cw["windspeed"]
65-
self.code = self.code_to_text(cw["weather_code"])
64+
def get(self, v, cw, ind):
65+
if ind == None:
66+
return cw[v]
67+
else:
68+
return cw[v][ind]
69+
70+
def full(self):
71+
return f"{self.code}\nTemp {self.temp:.1f} dew {self.dew:.1f} pres {self.pres:1f}\n" \
72+
f"Precip {self.precip}\nWind {self.wind} gust {self.gust}"
73+
74+
def short(self):
75+
r = f"{self.code} {self.temp:.1f}°C"
76+
if self.dew + 3 > self.temp:
77+
r += f" dew {self.dew:.1f}°C"
78+
if self.gust > self.wind + 5:
79+
r += f" {self.gust:.0f} g"
80+
elif self.wind > 10:
81+
r += f" {self.wind:.0f} w"
82+
# FIXME: add precip
83+
return r
84+
85+
def similar(self, prev):
86+
if self.code != prev.code:
87+
return False
88+
if abs(self.temp - prev.temp) > 3:
89+
return False
90+
if abs(self.wind - prev.wind) > 10:
91+
return False
92+
if abs(self.gust - prev.gust) > 10:
93+
return False
94+
return True
6695

6796
def summarize(self):
68-
return f"{self.code}\nTemp {self.temp}\nWind {self.wind}"
97+
return self.ftime() + self.short()
98+
99+
class Hourly(WData):
100+
def init(self, cw, ind):
101+
super().init()
102+
self.time = None
103+
self.temp = self.get("temperature_2m", cw, ind)
104+
self.dew = self.get("dewpoint_2m", cw, ind)
105+
self.pres = self.get("pressure_msl", cw, ind)
106+
self.precip = self.get("precipitation", cw, ind)
107+
self.wind = self.get("wind_speed_10m", cw, ind)
108+
self.gust = self.get("wind_gusts_10m", cw, ind)
109+
self.raw_code = self.get("weather_code", cw, ind)
110+
self.code = self.code_to_text(self.raw_code)
111+
112+
def ftime(self):
113+
if self.time:
114+
return self.time[11:13] + "h "
115+
return ""
116+
117+
class Daily(WData):
118+
def init(self, cw, ind):
119+
super().init()
120+
self.temp = self.get("temperature_2m_max", cw, ind)
121+
self.temp_min = self.get("temperature_2m_min", cw, ind)
122+
self.dew = self.get("dewpoint_2m_max", cw, ind)
123+
self.dew_min = self.get("dewpoint_2m_min", cw, ind)
124+
self.pres = None
125+
self.precip = self.get("precipitation_sum", cw, ind)
126+
self.wind = self.get("wind_speed_10m_max", cw, ind)
127+
self.gust = self.get("wind_gusts_10m_max", cw, ind)
128+
self.raw_code = self.get("weather_code", cw, ind)
129+
self.code = self.code_to_text(self.raw_code)
130+
131+
def ftime(self):
132+
return self.time[8:10] + ". "
69133

70134
class Weather:
71135
name = "Prague"
72-
lat = 50.08
73-
lon = 14.44
136+
# LKPR airport
137+
lat = 50 + 6/60.
138+
lon = 14 + 15/60.
74139

75140
def __init__(self):
76141
self.now = None
@@ -84,68 +149,102 @@ def fetch(self):
84149
# See https://open-meteo.com/en/docs?forecast_days=1&current=relative_humidity_2m
85150

86151
host = "api.open-meteo.com"
87-
port = 80 # HTTP only
88152
path = (
89153
"/v1/forecast?"
90154
"latitude={}&longitude={}"
91-
"&current=temperature_2m,dewpoint_2m,pressure_msl,precipitation,weather_code,windspeed"
155+
"&current=temperature_2m,dewpoint_2m,pressure_msl,precipitation,weather_code,wind_speed_10m,wind_gusts_10m"
156+
"&forecast_hours=8"
157+
"&hourly=temperature_2m,dewpoint_2m,pressure_msl,precipitation,weather_code,wind_speed_10m,wind_gusts_10m"
158+
"&forecast_days=10"
159+
"&daily=temperature_2m_max,temperature_2m_min,dewpoint_2m_min,dewpoint_2m_max,pressure_msl_min,pressure_msl_max,precipitation_sum,weather_code,wind_speed_10m_max,wind_gusts_10m_max"
92160
"&timezone=auto"
93161
).format(self.lat, self.lon)
94162

95163
print("Weather fetch: ", path)
164+
data = DownloadManager.download_url("https://"+host+path)
165+
if not data:
166+
self.summary = "Download error"
167+
return
168+
169+
#print("Have result:", body.decode())
96170

97-
# Resolve DNS
98-
addr = socket.getaddrinfo(host, port, socket.AF_INET)[0][-1]
99-
print("DNS", addr)
100-
101-
s = socket.socket()
102-
s.connect(addr)
103-
104-
# Send HTTP request
105-
request = (
106-
"GET {} HTTP/1.1\r\n"
107-
"Host: {}\r\n"
108-
"Connection: close\r\n\r\n"
109-
).format(path, host)
110-
111-
s.send(request.encode())
112-
113-
# ---- Read response ----
114-
# Skip HTTP headers
115-
buffer = b""
116-
while True:
117-
chunk = s.recv(256)
118-
if not chunk:
119-
raise Exception("No response")
120-
buffer += chunk
121-
header_end = buffer.find(b"\r\n\r\n")
122-
if header_end != -1:
123-
body = buffer[header_end + 4:]
124-
break
125-
126-
127-
# Read remaining body
128-
while True:
129-
chunk = s.recv(512)
130-
if not chunk:
131-
break
132-
body += chunk
171+
# Parse JSON
172+
data = ujson.loads(data)
133173

134-
s.close()
174+
# ---- Extract data ----
175+
print("\n\n")
135176

136-
# Strip non-json parts
137-
body = body[5:]
138-
body = body[:-7]
177+
s = ""
139178

140-
print("Have result:", body.decode())
179+
print("---- ")
180+
cw = data["current"]
181+
self.now = Hourly()
182+
self.now.init(cw, None)
183+
prev = self.now
184+
t = self.now.summarize()
185+
s += t + "\n"
186+
print(t)
141187

142-
# Parse JSON
143-
data = ujson.loads(body)
188+
self.hourly = []
189+
d = data["hourly"]
190+
times = d["time"]
191+
#print(d)
192+
193+
print("---- ")
194+
for i in range(len(times)):
195+
h = Hourly()
196+
h.init(d, i)
197+
h.time = times[i]
198+
self.hourly.append(h)
199+
if not h.similar(prev):
200+
t = h.summarize()
201+
s += t + "\n"
202+
print(t)
203+
prev = h
144204

145-
# ---- Extract data ----
146-
cw = data["current"]
147-
self.now = Hourly(cw)
148-
self.summary = self.now.summarize()
205+
self.daily = []
206+
d = data["daily"]
207+
times = d["time"]
208+
#print(d)
209+
210+
print("---- ")
211+
for i in range(len(times)):
212+
h = Daily()
213+
h.init(d, i)
214+
h.time = times[i]
215+
self.daily.append(h)
216+
if i == 0:
217+
prev = h
218+
elif not h.similar(prev):
219+
t = h.summarize()
220+
s += t + "\n"
221+
print(t)
222+
prev = h
223+
224+
225+
self.summary = s
226+
227+
def summarize_future():
228+
now = utime.time()
229+
230+
# Rain detection in next 24h
231+
for h in weather.hourly[:24]:
232+
if h["precip"] >= 1.0:
233+
return "Rain soon"
234+
235+
# Temperature trend
236+
if len(weather.hourly) > 24:
237+
t0 = weather.hourly[0]["temp"]
238+
t24 = weather.hourly[24]["temp"]
239+
if abs(t24 - t0) < 2:
240+
return "No change expected"
241+
if t24 > t0:
242+
return "Getting warmer"
243+
else:
244+
return "Getting cooler"
245+
246+
return "Stable weather"
247+
149248

150249
weather = Weather()
151250

@@ -167,32 +266,38 @@ def onCreate(self):
167266

168267
# ---- MAIN SCREEN ----
169268

269+
label_weather = lv.label(scr_main)
270+
label_weather.set_text(f"{weather.name} ({weather.lat}, {weather.lon})")
271+
label_weather.align(lv.ALIGN.TOP_LEFT, 10, 24)
272+
label_weather.set_style_text_font(lv.font_montserrat_14, 0)
273+
self.label_weather = label_weather
274+
275+
btn_hourly = lv.button(scr_main)
276+
btn_hourly.align(lv.ALIGN.TOP_RIGHT, -5, 24)
277+
lv.label(btn_hourly).set_text("Reload")
278+
btn_hourly.add_event_cb(lambda x: self.do_load(), lv.EVENT.CLICKED, None)
279+
170280
label_time = lv.label(scr_main)
171281
label_time.set_text("(time)")
172-
label_time.align(lv.ALIGN.TOP_LEFT, 10, 40)
282+
label_time.align_to(btn_hourly, lv.ALIGN.TOP_LEFT, -85, -10)
173283
label_time.set_style_text_font(lv.font_montserrat_24, 0)
174284
self.label_time = label_time
175285

176-
label_weather = lv.label(scr_main)
177-
label_weather.set_text(f"Weather for {weather.name} ({weather.lat}, {weather.lon})")
178-
label_weather.align_to(label_time, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10)
179-
label_weather.set_style_text_font(lv.font_montserrat_14, 0)
180-
self.label_weather = label_weather
181-
182286
label_summary = lv.label(scr_main)
183287
label_summary.set_text("(weather)")
184288
#label_summary.set_long_mode(lv.label.LONG.WRAP)
185-
label_summary.set_width(300)
289+
#label_summary.set_width(300)
186290
label_summary.align_to(label_weather, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 5)
187291
label_summary.set_style_text_font(lv.font_montserrat_24, 0)
188292
self.label_summary = label_summary
189293

190-
btn_hourly = lv.button(scr_main)
191-
btn_hourly.set_size(100, 40)
192-
btn_hourly.align(lv.ALIGN.BOTTOM_LEFT, 10, -10)
193-
lv.label(btn_hourly).set_text("Reload")
194294

195-
btn_hourly.add_event_cb(lambda x: self.do_load(), lv.EVENT.CLICKED, None)
295+
if False:
296+
btn_daily = lv.button(scr_main)
297+
btn_daily.set_size(100, 40)
298+
btn_daily.align(lv.ALIGN.BOTTOM_RIGHT, -10, -10)
299+
lv.label(btn_daily).set_text("Daily")
300+
196301

197302
self.setContentView(self.screen)
198303

@@ -223,3 +328,87 @@ def do_load(self):
223328
self.label_summary.set_text("Requesting...")
224329
weather.fetch()
225330

331+
# --------------------
332+
333+
def code():
334+
# -----------------------------
335+
# LVGL UI
336+
# -----------------------------
337+
338+
scr_main = lv.obj()
339+
scr_hourly = lv.obj()
340+
scr_daily = lv.obj()
341+
342+
343+
# ---- HOURLY SCREEN ----
344+
345+
hourly_list = lv.list(scr_hourly)
346+
hourly_list.set_size(320, 200)
347+
hourly_list.align(lv.ALIGN.TOP_MID, 0, 10)
348+
349+
btn_back1 = lv.button(scr_hourly)
350+
btn_back1.set_size(80, 30)
351+
btn_back1.align(lv.ALIGN.BOTTOM_MID, 0, -5)
352+
lv.label(btn_back1).set_text("Back")
353+
354+
# ---- DAILY SCREEN ----
355+
356+
daily_list = lv.list(scr_daily)
357+
daily_list.set_size(320, 200)
358+
daily_list.align(lv.ALIGN.TOP_MID, 0, 10)
359+
360+
btn_back2 = lv.button(scr_daily)
361+
btn_back2.set_size(80, 30)
362+
btn_back2.align(lv.ALIGN.BOTTOM_MID, 0, -5)
363+
lv.label(btn_back2).set_text("Back")
364+
365+
def foo():
366+
btn_hourly.add_event_cb(go_hourly, lv.EVENT.CLICKED, None)
367+
btn_daily.add_event_cb(go_daily, lv.EVENT.CLICKED, None)
368+
btn_back1.add_event_cb(go_back, lv.EVENT.CLICKED, None)
369+
btn_back2.add_event_cb(go_back, lv.EVENT.CLICKED, None)
370+
371+
# -----------------------------
372+
# STARTUP
373+
# -----------------------------
374+
375+
def go_hourly(e):
376+
populate_hourly()
377+
lv.scr_load(scr_hourly)
378+
379+
def go_daily(e):
380+
populate_daily()
381+
lv.scr_load(scr_daily)
382+
383+
def go_back(e):
384+
lv.scr_load(scr_main)
385+
386+
def update_ui():
387+
if weather.current_temp is not None:
388+
text = "%s %.1f C" % (
389+
weather_code_to_text(weather.current_code),
390+
weather.current_temp
391+
)
392+
label_weather.set_text(text)
393+
394+
label_summary.set_text(weather.summary)
395+
396+
def populate_hourly():
397+
hourly_list.clean()
398+
for h in weather.hourly[:24]:
399+
line = "%s %.1fC %.1fmm" % (
400+
h["time"][11:16],
401+
h["temp"],
402+
h["precip"]
403+
)
404+
hourly_list.add_text(line)
405+
406+
def populate_daily():
407+
daily_list.clean()
408+
for d in weather.daily:
409+
line = "%s %.1f/%.1f" % (
410+
d["date"],
411+
d["high"],
412+
d["low"]
413+
)
414+
daily_list.add_text(line)

0 commit comments

Comments
 (0)