commit 690069aa16b1a99e5859c60c72ae778c41dc75c4 Author: ritual Date: Sat Feb 21 16:47:13 2026 +0000 Adding visitor stats app diff --git a/visitor-stats/__init__.py b/visitor-stats/__init__.py new file mode 100644 index 0000000..0b00f23 --- /dev/null +++ b/visitor-stats/__init__.py @@ -0,0 +1,157 @@ +APP_DIR = "/system/apps/visitor_stats" +import sys +import os + +os.chdir(APP_DIR) +sys.path.insert(0, APP_DIR) + +from badgeware import run, State, rtc, set_brightness, clamp + +import wifi +import urequests + +# Enable RTC interrupts for hourly refresh +rtc.enable_timer_interrupt(True) +rtc.set_timer(150) + + +class AppState: + Display = 0 + ConnectWiFi = 1 + FetchData = 2 + Error = 3 + + +# Default state +state = { + "total_hits": 0, + "unique_visitors": 0, + "last_updated": "Never", + "font": "ignore", + "brightness": 0.1, +} +State.load("visitor_stats", state) + +set_brightness(state["brightness"]) +font_list = dir(rom_font) +font_index = font_list.index(state["font"]) +app_state = AppState.Display +error_message = "" +API_URL = "https://api.ritual.sh/analytics/stats" + + +def fetch_stats(): + try: + print(f"Fetching from {API_URL}") + response = urequests.get(API_URL) + print(f"Response status: {response.status_code}") + if response.status_code == 200: + data = response.json() + print(f"Data received: {data}") + response.close() + return data + response.close() + except Exception as e: + print(f"Fetch error: {e}") + return None + + +def format_number(n): + if n >= 1_000_000: + value = n / 1000000 + return f"{value:.1f}".rstrip("0").rstrip(".") + "m" + elif n >= 1_000: + value = n / 1000 + return f"{value:.1f}".rstrip("0").rstrip(".") + "k" + else: + return str(n) + + +def draw_display(): + # Pen variables + screen.pen = color.white + + ## Variables + visitor_count = format_number(state["total_hits"]) + unique_count = format_number(state["unique_visitors"]) + "d" + + screen.font = rom_font.troll + visitor_size = screen.measure_text(visitor_count) + visitor_x = (screen.width - visitor_size[0]) // 2 + screen.text(visitor_count, vec2(visitor_x, -4)) + + screen.font = rom_font.desert + unique_size = screen.measure_text(unique_count) + unique_x = (screen.width - unique_size[0]) // 2 + screen.text(unique_count, vec2(unique_x, visitor_size[1] - 10)) + + +def update(): + """Main update loop.""" + global app_state, state, error_message, font_index + wifi.tick() + + # Brightness controls + if io.BUTTON_UP in io.pressed: + state["brightness"] += 0.1 + if io.BUTTON_DOWN in io.pressed: + state["brightness"] -= 0.1 + state["brightness"] = clamp(state["brightness"], 0.1, 1.0) + set_brightness(state["brightness"]) + + # Manual refresh with button B + if io.BUTTON_B in io.pressed: + print("Button B pressed - manual refresh") + app_state = AppState.ConnectWiFi + + if rtc.read_timer_flag(): + print("RTC timer fired - auto refresh") + rtc.clear_timer_flag() + rtc.set_timer(150) + app_state = AppState.ConnectWiFi + # State machine + if app_state == AppState.Display: + draw_display() + elif app_state == AppState.ConnectWiFi: + draw_display() + print("Connecting to WiFi...") + if wifi.connect(): + print("WiFi connected") + app_state = AppState.FetchData + else: + # WiFi failed - silently revert to display with old data + print("WiFi connection failed - keeping old data") + app_state = AppState.Display + elif app_state == AppState.FetchData: + draw_display() + print("WiFi connected, fetching data...") + data = fetch_stats() + print(f"Result: {data}") + if data: + state["total_hits"] = data.get("totalHits", 0) + state["unique_visitors"] = data.get("uniqueVisitors", 0) + # Extract time from timestamp (format: "2026-01-29 15:33:29") + ts = data.get("lastUpdated", "") + if " " in ts: + state["last_updated"] = ts.split(" ")[1][:5] # HH:MM + else: + state["last_updated"] = "--:--" + print(f"Updated state: {state}") + State.save("visitor_stats", state) + else: + # Fetch failed - silently revert to display with old data + print("Fetch failed - keeping old data") + app_state = AppState.Display + elif app_state == AppState.Error: + # Any button press returns to display + if io.pressed: + app_state = AppState.Display + + +def on_exit(): + """Save state when exiting.""" + State.save("visitor_stats", state) + + +if __name__ == "__main__": + run(update=update, on_exit=on_exit) diff --git a/visitor-stats/icon.png b/visitor-stats/icon.png new file mode 100644 index 0000000..45d1a9c Binary files /dev/null and b/visitor-stats/icon.png differ