From 4646800c2736e9946bb74345a908bcf286141805 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sun, 22 Feb 2026 10:42:21 +0100 Subject: [PATCH] weather: start simple weather application Simple weather application, using open-meteo.com data. Once an hour weather is fetched and temperature/wind/sky condition is displayed. --- .../META-INF/MANIFEST.JSON | 24 ++ .../apps/cz.ucw.pavel.weather/assets/main.py | 225 ++++++++++++++++++ .../res/mipmap-mdpi/icon_64x64.png | Bin 0 -> 12342 bytes 3 files changed, 249 insertions(+) create mode 100644 internal_filesystem/apps/cz.ucw.pavel.weather/META-INF/MANIFEST.JSON create mode 100644 internal_filesystem/apps/cz.ucw.pavel.weather/assets/main.py create mode 100644 internal_filesystem/apps/cz.ucw.pavel.weather/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/apps/cz.ucw.pavel.weather/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.weather/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..53b80338 --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.weather/META-INF/MANIFEST.JSON @@ -0,0 +1,24 @@ +{ +"name": "Weather", +"publisher": "Pavel Machek", +"short_description": "Display weather information.", +"long_description": "This displays weather information from open-meteo.com.", +"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.weather/icons/cz.ucw.pavel.weather_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.weather/mpks/cz.ucw.pavel.weather_0.0.1.mpk", +"fullname": "cz.ucw.pavel.weather", +"version": "0.0.1", +"category": "utilities", +"activities": [ + { + "entrypoint": "assets/main.py", + "classname": "Main", + "intent_filters": [ + { + "action": "main", + "category": "launcher" + } + ] + } + ] +} + diff --git a/internal_filesystem/apps/cz.ucw.pavel.weather/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.weather/assets/main.py new file mode 100644 index 00000000..9258e47d --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.weather/assets/main.py @@ -0,0 +1,225 @@ +from mpos import Activity + +""" +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. +""" + +import time +import os + +try: + import lvgl as lv +except ImportError: + pass + +from mpos import Activity, MposKeyboard + +import ujson +import utime +import usocket as socket +import ujson + +# ----------------------------- +# WEATHER DATA MODEL +# ----------------------------- + +class WData: + WMO_CODES = { + 0: "Clear sky", + 1: "Mainly clear", + 2: "Partly cloudy", + 3: "Overcast", + 45: "Fog", + 48: "Rime fog", + 51: "Light drizzle", + 53: "Drizzle", + 55: "Heavy drizzle", + 56: "Freezing drizzle", + 57: "Freezing drizzle", + 61: "Light rain", + 63: "Rain", + 65: "Heavy rain", + 66: "Freezing rain", + 67: "Freezing rain", + 71: "Light snow", + 73: "Snow", + 75: "Heavy snow", + 77: "Snow grains", + 80: "Rain showers", + 81: "Rain showers", + 82: "Heavy rain showers", + 85: "Snow showers", + 86: "Heavy snow showers", + 95: "Thunderstorm", + 96: "Thunderstorm + hail", + 99: "Thunderstorm + hail", + } + + def code_to_text(self, code): + return self.WMO_CODES.get(int(code), "Unknown") + +class Hourly(WData): + def __init__(self, cw): + self.temp = cw["temperature_2m"] + self.wind = cw["windspeed"] + self.code = self.code_to_text(cw["weather_code"]) + + def summarize(self): + return f"{self.code}\nTemp {self.temp}\nWind {self.wind}" + +class Weather: + name = "Prague" + lat = 50.08 + lon = 14.44 + + def __init__(self): + self.now = None + self.hourly = [] + self.daily = [] + self.summary = "(no weather)" + + def fetch(self): + self.summary = "...fetching..." + + # See https://open-meteo.com/en/docs?forecast_days=1¤t=relative_humidity_2m + + host = "api.open-meteo.com" + port = 80 # HTTP only + path = ( + "/v1/forecast?" + "latitude={}&longitude={}" + "¤t=temperature_2m,dewpoint_2m,pressure_msl,precipitation,weather_code,windspeed" + "&timezone=auto" + ).format(self.lat, self.lon) + + print("Weather fetch: ", path) + + # Resolve DNS + addr = socket.getaddrinfo(host, port, socket.AF_INET)[0][-1] + print("DNS", addr) + + s = socket.socket() + s.connect(addr) + + # Send HTTP request + request = ( + "GET {} HTTP/1.1\r\n" + "Host: {}\r\n" + "Connection: close\r\n\r\n" + ).format(path, host) + + s.send(request.encode()) + + # ---- Read response ---- + # Skip HTTP headers + buffer = b"" + while True: + chunk = s.recv(256) + if not chunk: + raise Exception("No response") + buffer += chunk + header_end = buffer.find(b"\r\n\r\n") + if header_end != -1: + body = buffer[header_end + 4:] + break + + + # Read remaining body + while True: + chunk = s.recv(512) + if not chunk: + break + body += chunk + + s.close() + + # Strip non-json parts + body = body[5:] + body = body[:-7] + + print("Have result:", body.decode()) + + # Parse JSON + data = ujson.loads(body) + + # ---- Extract data ---- + cw = data["current"] + self.now = Hourly(cw) + self.summary = self.now.summarize() + +weather = Weather() + +# ------------------------------------------------------------ +# Main activity +# ------------------------------------------------------------ + +class Main(Activity): + def __init__(self): + self.last_hour = 0 + super().__init__() + + # -------------------- + + def onCreate(self): + self.screen = lv.obj() + #self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) + scr_main = self.screen + + # ---- MAIN SCREEN ---- + + label_time = lv.label(scr_main) + label_time.set_text("(time)") + label_time.align(lv.ALIGN.TOP_LEFT, 10, 40) + label_time.set_style_text_font(lv.font_montserrat_24, 0) + self.label_time = label_time + + label_weather = lv.label(scr_main) + label_weather.set_text(f"Weather for {weather.name} ({weather.lat}, {weather.lon})") + label_weather.align_to(label_time, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) + label_weather.set_style_text_font(lv.font_montserrat_14, 0) + self.label_weather = label_weather + + label_summary = lv.label(scr_main) + label_summary.set_text("(weather)") + #label_summary.set_long_mode(lv.label.LONG.WRAP) + label_summary.set_width(300) + label_summary.align_to(label_weather, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 5) + label_summary.set_style_text_font(lv.font_montserrat_24, 0) + self.label_summary = label_summary + + btn_hourly = lv.button(scr_main) + btn_hourly.set_size(100, 40) + btn_hourly.align(lv.ALIGN.BOTTOM_LEFT, 10, -10) + lv.label(btn_hourly).set_text("Reload") + + btn_hourly.add_event_cb(lambda x: self.do_load(), lv.EVENT.CLICKED, None) + + self.setContentView(self.screen) + + def onResume(self, screen): + self.timer = lv.timer_create(self.tick, 15000, None) + self.tick(0) + + def onPause(self, screen): + if self.timer: + self.timer.delete() + self.timer = None + + # -------------------- + + def tick(self, t): + now = time.localtime() + y, m, d = now[0], now[1], now[2] + hh, mm, ss = now[3], now[4], now[5] + + if hh != self.last_hour: + self.last_hour = hh + self.do_load() + + self.label_time.set_text("%02d:%02d" % (hh, mm)) + self.label_summary.set_text(weather.summary) + + def do_load(self): + self.label_summary.set_text("Requesting...") + weather.fetch() + diff --git a/internal_filesystem/apps/cz.ucw.pavel.weather/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/apps/cz.ucw.pavel.weather/res/mipmap-mdpi/icon_64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..87df6b6275aeea67edcfb9f9b101568b579dcf41 GIT binary patch literal 12342 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 Xt^h&B!MW?jt4~{9U#(Kb{@#B9<^11~ literal 0 HcmV?d00001