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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