|
| 1 | +from mpos import Activity |
| 2 | + |
| 3 | +""" |
| 4 | +Look at https://open-meteo.com/en/docs , then design an application that would display current time and weather, and summary of forecast ("no change expected for 2 days" or maybe "rain in 5 hours"), with a way to access detailed forecast. |
| 5 | +""" |
| 6 | + |
| 7 | +import time |
| 8 | +import os |
| 9 | + |
| 10 | +try: |
| 11 | + import lvgl as lv |
| 12 | +except ImportError: |
| 13 | + pass |
| 14 | + |
| 15 | +from mpos import Activity, MposKeyboard |
| 16 | + |
| 17 | +import ujson |
| 18 | +import utime |
| 19 | +import usocket as socket |
| 20 | +import ujson |
| 21 | + |
| 22 | +# ----------------------------- |
| 23 | +# WEATHER DATA MODEL |
| 24 | +# ----------------------------- |
| 25 | + |
| 26 | +class WData: |
| 27 | + WMO_CODES = { |
| 28 | + 0: "Clear sky", |
| 29 | + 1: "Mainly clear", |
| 30 | + 2: "Partly cloudy", |
| 31 | + 3: "Overcast", |
| 32 | + 45: "Fog", |
| 33 | + 48: "Rime fog", |
| 34 | + 51: "Light drizzle", |
| 35 | + 53: "Drizzle", |
| 36 | + 55: "Heavy drizzle", |
| 37 | + 56: "Freezing drizzle", |
| 38 | + 57: "Freezing drizzle", |
| 39 | + 61: "Light rain", |
| 40 | + 63: "Rain", |
| 41 | + 65: "Heavy rain", |
| 42 | + 66: "Freezing rain", |
| 43 | + 67: "Freezing rain", |
| 44 | + 71: "Light snow", |
| 45 | + 73: "Snow", |
| 46 | + 75: "Heavy snow", |
| 47 | + 77: "Snow grains", |
| 48 | + 80: "Rain showers", |
| 49 | + 81: "Rain showers", |
| 50 | + 82: "Heavy rain showers", |
| 51 | + 85: "Snow showers", |
| 52 | + 86: "Heavy snow showers", |
| 53 | + 95: "Thunderstorm", |
| 54 | + 96: "Thunderstorm + hail", |
| 55 | + 99: "Thunderstorm + hail", |
| 56 | + } |
| 57 | + |
| 58 | + def code_to_text(self, code): |
| 59 | + return self.WMO_CODES.get(int(code), "Unknown") |
| 60 | + |
| 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"]) |
| 66 | + |
| 67 | + def summarize(self): |
| 68 | + return f"{self.code}\nTemp {self.temp}\nWind {self.wind}" |
| 69 | + |
| 70 | +class Weather: |
| 71 | + name = "Prague" |
| 72 | + lat = 50.08 |
| 73 | + lon = 14.44 |
| 74 | + |
| 75 | + def __init__(self): |
| 76 | + self.now = None |
| 77 | + self.hourly = [] |
| 78 | + self.daily = [] |
| 79 | + self.summary = "(no weather)" |
| 80 | + |
| 81 | + def fetch(self): |
| 82 | + self.summary = "...fetching..." |
| 83 | + |
| 84 | + # See https://open-meteo.com/en/docs?forecast_days=1¤t=relative_humidity_2m |
| 85 | + |
| 86 | + host = "api.open-meteo.com" |
| 87 | + port = 80 # HTTP only |
| 88 | + path = ( |
| 89 | + "/v1/forecast?" |
| 90 | + "latitude={}&longitude={}" |
| 91 | + "¤t=temperature_2m,dewpoint_2m,pressure_msl,precipitation,weather_code,windspeed" |
| 92 | + "&timezone=auto" |
| 93 | + ).format(self.lat, self.lon) |
| 94 | + |
| 95 | + print("Weather fetch: ", path) |
| 96 | + |
| 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 |
| 133 | + |
| 134 | + s.close() |
| 135 | + |
| 136 | + # Strip non-json parts |
| 137 | + body = body[5:] |
| 138 | + body = body[:-7] |
| 139 | + |
| 140 | + print("Have result:", body.decode()) |
| 141 | + |
| 142 | + # Parse JSON |
| 143 | + data = ujson.loads(body) |
| 144 | + |
| 145 | + # ---- Extract data ---- |
| 146 | + cw = data["current"] |
| 147 | + self.now = Hourly(cw) |
| 148 | + self.summary = self.now.summarize() |
| 149 | + |
| 150 | +weather = Weather() |
| 151 | + |
| 152 | +# ------------------------------------------------------------ |
| 153 | +# Main activity |
| 154 | +# ------------------------------------------------------------ |
| 155 | + |
| 156 | +class Main(Activity): |
| 157 | + def __init__(self): |
| 158 | + self.last_hour = 0 |
| 159 | + super().__init__() |
| 160 | + |
| 161 | + # -------------------- |
| 162 | + |
| 163 | + def onCreate(self): |
| 164 | + self.screen = lv.obj() |
| 165 | + #self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) |
| 166 | + scr_main = self.screen |
| 167 | + |
| 168 | + # ---- MAIN SCREEN ---- |
| 169 | + |
| 170 | + label_time = lv.label(scr_main) |
| 171 | + label_time.set_text("(time)") |
| 172 | + label_time.align(lv.ALIGN.TOP_LEFT, 10, 40) |
| 173 | + label_time.set_style_text_font(lv.font_montserrat_24, 0) |
| 174 | + self.label_time = label_time |
| 175 | + |
| 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 | + |
| 182 | + label_summary = lv.label(scr_main) |
| 183 | + label_summary.set_text("(weather)") |
| 184 | + #label_summary.set_long_mode(lv.label.LONG.WRAP) |
| 185 | + label_summary.set_width(300) |
| 186 | + label_summary.align_to(label_weather, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 5) |
| 187 | + label_summary.set_style_text_font(lv.font_montserrat_24, 0) |
| 188 | + self.label_summary = label_summary |
| 189 | + |
| 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") |
| 194 | + |
| 195 | + btn_hourly.add_event_cb(lambda x: self.do_load(), lv.EVENT.CLICKED, None) |
| 196 | + |
| 197 | + self.setContentView(self.screen) |
| 198 | + |
| 199 | + def onResume(self, screen): |
| 200 | + self.timer = lv.timer_create(self.tick, 15000, None) |
| 201 | + self.tick(0) |
| 202 | + |
| 203 | + def onPause(self, screen): |
| 204 | + if self.timer: |
| 205 | + self.timer.delete() |
| 206 | + self.timer = None |
| 207 | + |
| 208 | + # -------------------- |
| 209 | + |
| 210 | + def tick(self, t): |
| 211 | + now = time.localtime() |
| 212 | + y, m, d = now[0], now[1], now[2] |
| 213 | + hh, mm, ss = now[3], now[4], now[5] |
| 214 | + |
| 215 | + if hh != self.last_hour: |
| 216 | + self.last_hour = hh |
| 217 | + self.do_load() |
| 218 | + |
| 219 | + self.label_time.set_text("%02d:%02d" % (hh, mm)) |
| 220 | + self.label_summary.set_text(weather.summary) |
| 221 | + |
| 222 | + def do_load(self): |
| 223 | + self.label_summary.set_text("Requesting...") |
| 224 | + weather.fetch() |
| 225 | + |
0 commit comments