Created
October 7, 2025 19:55
-
-
Save Staars/93a4f53d980777a7307f9ee7369da12a to your computer and use it in GitHub Desktop.
Apple Style Weather App
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class APPLE_WEATHER_APP : Driver | |
| var data_buffers | |
| var update_index | |
| var pending_widget | |
| var current_values | |
| var last_update | |
| var hourly_forecast | |
| var daily_forecast | |
| static BUFFER_SIZE = 24 | |
| static UPDATE_INTERVAL = 300000 | |
| def init() | |
| self.update_index = 0 | |
| self.pending_widget = nil | |
| self.last_update = 0 | |
| self.data_buffers = {} | |
| self.hourly_forecast = [] | |
| self.daily_forecast = [] | |
| # Apple Weather style current values | |
| self.current_values = { | |
| 'temperature': 22, | |
| 'condition': 'Partly Cloudy', | |
| 'high': 26, | |
| 'low': 18, | |
| 'feels_like': 23, | |
| 'humidity': 65, | |
| 'wind_speed': 12, | |
| 'wind_direction': 'SW', | |
| 'uv_index': 4, | |
| 'visibility': 16, | |
| 'pressure': 1013, | |
| 'location': 'Current Location', | |
| 'sunrise': '6:45 AM', | |
| 'sunset': '7:30 PM', | |
| 'precipitation': 10 | |
| } | |
| # Initialize data buffers | |
| var metrics = ['temperature', 'humidity', 'pressure'] | |
| for metric: metrics | |
| var buffer = [] | |
| var i = 0 | |
| while i < self.BUFFER_SIZE | |
| buffer.push(0) | |
| i += 1 | |
| end | |
| self.data_buffers[metric] = buffer | |
| end | |
| tasmota.add_driver(self) | |
| self.fetch_weather_data() | |
| end | |
| def every_100ms() | |
| self.update_next_widget() | |
| end | |
| def every_second() | |
| var now = tasmota.millis() | |
| if now - self.last_update > self.UPDATE_INTERVAL | |
| self.fetch_weather_data() | |
| self.last_update = now | |
| end | |
| end | |
| def fetch_weather_data() | |
| import json | |
| var lat_cmd = tasmota.cmd("Latitude") | |
| var lon_cmd = tasmota.cmd("Longitude") | |
| var lat = nil | |
| var lon = nil | |
| if lat_cmd != nil && lat_cmd.find("Latitude") != nil | |
| lat = real(lat_cmd["Latitude"]) | |
| end | |
| if lon_cmd != nil && lon_cmd.find("Longitude") != nil | |
| lon = real(lon_cmd["Longitude"]) | |
| end | |
| if lat == nil || lon == nil | |
| print("Weather: No location configured, using demo data") | |
| self.generate_demo_data() | |
| return | |
| end | |
| var url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t=temperature_2m,relative_humidity_2m,pressure_msl,wind_speed_10m,wind_direction_10m,weather_code,precipitation&hourly=temperature_2m,relative_humidity_2m,weather_code,precipitation&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset&timezone=auto" | |
| var cl = webclient() | |
| cl.begin(url) | |
| var result = cl.GET() | |
| if result == 200 | |
| var response = cl.get_string() | |
| if size(response) > 0 | |
| var data = json.load(response) | |
| if data != nil | |
| self.parse_weather_data(data) | |
| print("Weather: Data updated successfully") | |
| return | |
| end | |
| end | |
| end | |
| print(f"Weather: API error {result}, using demo data") | |
| self.generate_demo_data() | |
| end | |
| def parse_weather_data(data) | |
| import math | |
| var current = data.find('current') | |
| if current == nil | |
| self.generate_demo_data() | |
| return | |
| end | |
| # Current conditions | |
| self.current_values['temperature'] = int(real(current.find('temperature_2m', 22))) | |
| self.current_values['humidity'] = int(real(current.find('relative_humidity_2m', 65))) | |
| self.current_values['pressure'] = int(real(current.find('pressure_msl', 1013))) | |
| self.current_values['wind_speed'] = int(real(current.find('wind_speed_10m', 12))) | |
| self.current_values['precipitation'] = int(real(current.find('precipitation', 10))) | |
| var wcode = int(real(current.find('weather_code', 2))) | |
| self.current_values['condition'] = self.weather_code_to_string(wcode) | |
| # Get highs/lows from daily data | |
| var daily = data.find('daily') | |
| if daily != nil | |
| var temp_max = daily.find('temperature_2m_max') | |
| var temp_min = daily.find('temperature_2m_min') | |
| if temp_max != nil && size(temp_max) > 0 | |
| self.current_values['high'] = int(real(temp_max[0])) | |
| end | |
| if temp_min != nil && size(temp_min) > 0 | |
| self.current_values['low'] = int(real(temp_min[0])) | |
| end | |
| # Sunrise/sunset | |
| var sunrise = daily.find('sunrise') | |
| var sunset = daily.find('sunset') | |
| if sunrise != nil && size(sunrise) > 0 | |
| self.current_values['sunrise'] = self.format_time(sunrise[0]) | |
| end | |
| if sunset != nil && size(sunset) > 0 | |
| self.current_values['sunset'] = self.format_time(sunset[0]) | |
| end | |
| end | |
| # Parse hourly forecast | |
| var hourly = data.find('hourly') | |
| if hourly != nil | |
| self.parse_hourly_forecast(hourly) | |
| end | |
| # Parse daily forecast | |
| if daily != nil | |
| self.parse_daily_forecast(daily) | |
| end | |
| # Update buffers | |
| self.update_buffer('temperature', self.current_values['temperature']) | |
| self.update_buffer('humidity', self.current_values['humidity']) | |
| self.update_buffer('pressure', self.current_values['pressure']) | |
| end | |
| def weather_code_to_string(wcode) | |
| if wcode == 0 | |
| return "Clear" | |
| elif wcode == 1 | |
| return "Mainly Clear" | |
| elif wcode == 2 | |
| return "Partly Cloudy" | |
| elif wcode == 3 | |
| return "Overcast" | |
| elif wcode >= 45 && wcode <= 48 | |
| return "Foggy" | |
| elif wcode >= 51 && wcode <= 67 | |
| return "Rain" | |
| elif wcode >= 71 && wcode <= 77 | |
| return "Snow" | |
| elif wcode >= 80 && wcode <= 86 | |
| return "Rain Showers" | |
| elif wcode >= 95 && wcode <= 99 | |
| return "Thunderstorm" | |
| else | |
| return "Partly Cloudy" | |
| end | |
| end | |
| def format_time(iso_time) | |
| import crypto | |
| var hour = (crypto.random(1)[0] % 12) + 1 | |
| var minute = crypto.random(1)[0] % 60 | |
| var am_pm = crypto.random(1)[0] % 2 == 0 ? "AM" : "PM" | |
| return f"{hour}:{minute:02d} {am_pm}" | |
| end | |
| def generate_demo_data() | |
| import crypto | |
| import math | |
| var time_ms = tasmota.millis() | |
| var time_factor = time_ms / 60000.0 | |
| # Demo current conditions | |
| self.current_values['temperature'] = int(20 + 8 * math.sin(time_factor * 0.01)) | |
| self.current_values['high'] = self.current_values['temperature'] + 4 | |
| self.current_values['low'] = self.current_values['temperature'] - 4 | |
| self.current_values['humidity'] = int(60 + 20 * math.sin(time_factor * 0.005)) | |
| self.current_values['wind_speed'] = int(5 + 10 * math.abs(math.sin(time_factor * 0.003))) | |
| self.current_values['precipitation'] = crypto.random(1)[0] % 100 | |
| # Demo forecasts | |
| self.generate_demo_forecasts() | |
| # Update buffers | |
| self.update_buffer('temperature', self.current_values['temperature']) | |
| self.update_buffer('humidity', self.current_values['humidity']) | |
| self.update_buffer('pressure', self.current_values['pressure']) | |
| end | |
| def generate_demo_forecasts() | |
| import crypto | |
| import math | |
| # Generate hourly forecast (24 hours) | |
| self.hourly_forecast = [] | |
| var base_temp = self.current_values['temperature'] | |
| var i = 0 | |
| while i < 24 | |
| var hour_temp = base_temp + 5 * math.sin(i * 0.3) + (crypto.random(1)[0] % 3) | |
| var condition = crypto.random(1)[0] % 5 | |
| self.hourly_forecast.push({ | |
| 'hour': i, | |
| 'temperature': int(hour_temp), | |
| 'condition': condition | |
| }) | |
| i += 1 | |
| end | |
| # Generate daily forecast (7 days) | |
| self.daily_forecast = [] | |
| i = 0 | |
| while i < 7 | |
| var day_temp = base_temp + 2 * math.sin(i * 0.5) + (crypto.random(1)[0] % 4) | |
| var condition = crypto.random(1)[0] % 5 | |
| self.daily_forecast.push({ | |
| 'day': i, | |
| 'high': int(day_temp + 3), | |
| 'low': int(day_temp - 3), | |
| 'condition': condition | |
| }) | |
| i += 1 | |
| end | |
| end | |
| def parse_hourly_forecast(hourly_data) | |
| self.hourly_forecast = [] | |
| var times = hourly_data.find('time') | |
| var temps = hourly_data.find('temperature_2m') | |
| var weather_codes = hourly_data.find('weather_code') | |
| if times != nil && temps != nil | |
| var i = 0 | |
| while i < size(times) && i < 24 | |
| var condition = 2 | |
| if weather_codes != nil && i < size(weather_codes) | |
| condition = self.weather_code_to_condition(weather_codes[i]) | |
| end | |
| self.hourly_forecast.push({ | |
| 'hour': i, | |
| 'temperature': int(real(temps[i])), | |
| 'condition': condition | |
| }) | |
| i += 1 | |
| end | |
| end | |
| end | |
| def parse_daily_forecast(daily_data) | |
| self.daily_forecast = [] | |
| var times = daily_data.find('time') | |
| var temps_max = daily_data.find('temperature_2m_max') | |
| var temps_min = daily_data.find('temperature_2m_min') | |
| var weather_codes = daily_data.find('weather_code') | |
| if times != nil && temps_max != nil && temps_min != nil | |
| var i = 0 | |
| while i < size(times) && i < 7 | |
| var condition = 2 | |
| if weather_codes != nil && i < size(weather_codes) | |
| condition = self.weather_code_to_condition(weather_codes[i]) | |
| end | |
| self.daily_forecast.push({ | |
| 'day': i, | |
| 'high': int(real(temps_max[i])), | |
| 'low': int(real(temps_min[i])), | |
| 'condition': condition | |
| }) | |
| i += 1 | |
| end | |
| end | |
| end | |
| def weather_code_to_condition(wcode) | |
| if wcode == 0 | |
| return 0 # Clear | |
| elif wcode == 1 || wcode == 2 | |
| return 1 # Partly cloudy | |
| elif wcode == 3 | |
| return 2 # Cloudy | |
| elif wcode >= 51 && wcode <= 86 | |
| return 3 # Rain | |
| elif wcode >= 71 && wcode <= 77 | |
| return 4 # Snow | |
| else | |
| return 2 # Default cloudy | |
| end | |
| end | |
| def update_buffer(metric, value) | |
| if self.data_buffers.find(metric) != nil | |
| var buffer = self.data_buffers[metric] | |
| buffer.remove(0) | |
| buffer.push(int(value)) | |
| end | |
| end | |
| def update_next_widget() | |
| import MI32 | |
| if MI32.widget() == false | |
| return | |
| end | |
| var widget_num = (self.update_index % 3) + 1 # Reduced to 3 widgets now | |
| self.pending_widget = self.get_widget_content(widget_num) | |
| if MI32.widget(self.pending_widget) | |
| self.update_index += 1 | |
| end | |
| end | |
| def get_widget_content(widget_num) | |
| var content = "" | |
| var title = "" | |
| var box_class = "box w1 h2" | |
| if widget_num == 1 | |
| # Current Conditions + Gauges | |
| content = self.get_current_conditions_widget() + self.get_weather_gauges_widget() | |
| elif widget_num == 2 | |
| # Hourly Forecast + Weather Details | |
| content = self.get_hourly_forecast_widget() + self.get_weather_details_widget() | |
| elif widget_num == 3 | |
| # 10-Day Forecast | |
| content = self.get_daily_forecast_widget() | |
| end | |
| var widget = f'<div class="{box_class}" id=ww{widget_num} style="background: linear-gradient(135deg, #74b9ff, #0984e3); color: white; border-radius: 16px;">{content}</div>' | |
| return widget | |
| end | |
| def get_current_conditions_widget() | |
| var temp = self.current_values['temperature'] | |
| var condition = self.current_values['condition'] | |
| var high = self.current_values['high'] | |
| var low = self.current_values['low'] | |
| var location = self.current_values['location'] | |
| var emoji = self.get_condition_emoji(condition) | |
| var html = f'<div style="text-align: center; font-family: -apple-system, system-ui, sans-serif;">' | |
| '<div style="font-size: 12px; opacity: 0.9;">{location}</div>' | |
| '<div style="font-size: 48px; font-weight: 100;">{temp}°</div>' | |
| '<div style="font-size: 32px; margin-bottom: 5px;">{emoji} {condition}</div>' | |
| '<div style="font-size: 18px; opacity: 0.8;">H: {high}° L: {low}°</div>' | |
| '</div>' | |
| return html | |
| end | |
| def get_hourly_forecast_widget() | |
| var html = '<div style="font-family: -apple-system, system-ui, sans-serif;">' | |
| '<div style="font-size: 18px; font-weight: 600; text-align: center;">Hourly Forecast</div>' | |
| var i = 0 | |
| while i < size(self.hourly_forecast) && i < 12 | |
| var hour_data = self.hourly_forecast[i] | |
| var hour_label = i == 0 ? "Now" : f"{hour_data['hour']}:00" | |
| var emoji = self.get_condition_emoji_by_code(hour_data['condition']) | |
| html += f'<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.2);">' | |
| '<div style="width: 30px; font-size: 18px;">{hour_label}</div>' | |
| '<div style="font-size: 18px;">{emoji}</div>' | |
| '<div style="font-size: 18px; font-weight: 500;">{hour_data["temperature"]}°</div>' | |
| '</div>' | |
| i += 2 | |
| end | |
| html += '</div>' | |
| return html | |
| end | |
| def get_daily_forecast_widget() | |
| var html = '<div style="font-family: -apple-system, system-ui, sans-serif;">' | |
| '<div style="font-size: 18px; font-weight: 600; margin-bottom: 10px; text-align: center;">7-Day Forecast</div>' | |
| var days = ["Today", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] | |
| var i = 0 | |
| while i < size(self.daily_forecast) && i < 7 | |
| var day_data = self.daily_forecast[i] | |
| var emoji = self.get_condition_emoji_by_code(day_data['condition']) | |
| html += f'<div style="display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.2);">' | |
| '<div style="width: 40px; font-size: 16px; font-weight: 500;">{days[i]}</div>' | |
| '<div style="font-size: 16px;">{emoji}</div>' | |
| '<div style="display: flex; gap: 7px;">' | |
| '<span style="font-size: 16px; font-weight: 500;">{day_data["high"]}°</span>' | |
| '<span style="font-size: 16px; opacity: 0.7;">{day_data["low"]}°</span>' | |
| '</div>' | |
| '</div>' | |
| i += 1 | |
| end | |
| html += '</div>' | |
| return html | |
| end | |
| def get_weather_details_widget() | |
| var html = f'<div style="font-family: -apple-system, system-ui, sans-serif; margin-top: 15px;">' | |
| '<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 7px;">' | |
| # Left column | |
| '<div style="background: rgba(255,255,255,0.1); padding: 7px; border-radius: 6px; text-align: center;">' | |
| '<div style="font-size: 7px; opacity: 0.8;">FEELS LIKE</div>' | |
| '<div style="font-size: 12px; font-weight: 500;">{self.current_values["feels_like"]}°</div>' | |
| '</div>' | |
| '<div style="background: rgba(255,255,255,0.1); padding: 7px; border-radius: 6px; text-align: center;">' | |
| '<div style="font-size: 7px; opacity: 0.8;">HUMIDITY</div>' | |
| '<div style="font-size: 12px; font-weight: 500;">{self.current_values["humidity"]}%</div>' | |
| '</div>' | |
| # Right column | |
| '<div style="background: rgba(255,255,255,0.1); padding: 7px; border-radius: 6px; text-align: center;">' | |
| '<div style="font-size: 7px; opacity: 0.8;">WIND</div>' | |
| '<div style="font-size: 12px; font-weight: 500;">{self.current_values["wind_speed"]} km/h</div>' | |
| '</div>' | |
| '<div style="background: rgba(255,255,255,0.1); padding: 7px; border-radius: 6px; text-align: center;">' | |
| '<div style="font-size: 7px; opacity: 0.8;">SUNRISE</div>' | |
| '<div style="font-size: 9px; font-weight: 500;">{self.current_values["sunrise"]}</div>' | |
| '</div>' | |
| '</div>' | |
| '</div>' | |
| return html | |
| end | |
| def get_weather_gauges_widget() | |
| var html = '<div style="font-family: -apple-system, system-ui, sans-serif; margin-top: 15px;">' | |
| '<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; align-items: center;">' | |
| # Humidity Gauge - Blue scale (0-100%) | |
| var humidity = self.current_values['humidity'] | |
| var humidity_gauge = self.create_gauge(80, 80, humidity, 0, 100, | |
| 200, 220, 255, 30, # Light blue at 30% | |
| 150, 200, 255, 50, # Medium blue at 50% | |
| 100, 180, 255, 70, # Blue at 70% | |
| 50, 150, 255, 90, # Dark blue at 90% | |
| "%") | |
| html += f'<div style="text-align: center;"><div style="font-size: 7px; margin-bottom: 4px;">HUMIDITY</div>{humidity_gauge}</div>' | |
| # Pressure Gauge - Green to Orange (970-1040 hPa) | |
| var pressure = self.current_values['pressure'] | |
| var pressure_gauge = self.create_gauge(80, 80, pressure, 970, 1040, | |
| 50, 200, 50, 990, # Green at 990 | |
| 100, 255, 100, 1010, # Light green at 1010 | |
| 255, 255, 100, 1020, # Yellow at 1020 | |
| 255, 200, 50, 1030, # Orange at 1030 | |
| "hPa") | |
| html += f'<div style="text-align: center;"><div style="font-size: 7px; margin-bottom: 4px;">PRESSURE</div>{pressure_gauge}</div>' | |
| # Wind Speed Gauge - Calm to Storm (0-50 km/h) | |
| var wind_speed = self.current_values['wind_speed'] | |
| var wind_gauge = self.create_gauge(80, 80, wind_speed, 0, 50, | |
| 100, 255, 100, 10, # Green (calm) | |
| 150, 255, 150, 20, # Light green | |
| 255, 255, 100, 30, # Yellow (breezy) | |
| 255, 150, 50, 40, # Orange (windy) | |
| "km/h") | |
| html += f'<div style="text-align: center;"><div style="font-size: 7px; margin-bottom: 4px;">WIND SPEED</div>{wind_gauge}</div>' | |
| # Precipitation Chance Gauge - Blue scale (0-100%) | |
| var precip = self.current_values['precipitation'] | |
| var precip_gauge = self.create_gauge(80, 80, precip, 0, 100, | |
| 200, 220, 255, 20, # Very light blue | |
| 150, 200, 255, 40, # Light blue | |
| 100, 150, 255, 60, # Medium blue | |
| 50, 100, 255, 80, # Dark blue | |
| "%") | |
| html += f'<div style="text-align: center;"><div style="font-size: 7px; margin-bottom: 4px;">PRECIPITATION</div>{precip_gauge}</div>' | |
| '</div>' | |
| '</div>' | |
| return html | |
| end | |
| def create_gauge(width, height, value, min_val, max_val, r1, g1, b1, thresh1, r2, g2, b2, thresh2, r3, g3, b3, thresh3, r4, g4, b4, thresh4, unit) | |
| # Create gauge using the correct DSL format | |
| return f"{{g,{width},{height},{real(value)},{min_val},{max_val},{r1},{g1},{b1},{thresh1},{r2},{g2},{b2},{thresh2},{r3},{g3},{b3},{thresh3},{r4},{g4},{b4},{thresh4},{unit}}}" | |
| end | |
| def get_condition_emoji(condition) | |
| if condition == "Clear" || condition == "Mainly Clear" | |
| return "☀️" | |
| elif condition == "Partly Cloudy" | |
| return "⛅" | |
| elif condition == "Overcast" || condition == "Foggy" | |
| return "☁️" | |
| elif condition == "Rain" || condition == "Rain Showers" | |
| return "🌧️" | |
| elif condition == "Snow" | |
| return "❄️" | |
| elif condition == "Thunderstorm" | |
| return "⛈️" | |
| else | |
| return "🌈" | |
| end | |
| end | |
| def get_condition_emoji_by_code(code) | |
| if code == 0 | |
| return "☀️" | |
| elif code == 1 | |
| return "⛅" | |
| elif code == 2 | |
| return "☁️" | |
| elif code == 3 | |
| return "🌧️" | |
| elif code == 4 | |
| return "❄️" | |
| else | |
| return "🌈" | |
| end | |
| end | |
| def stop() | |
| tasmota.remove_driver(self) | |
| end | |
| end | |
| # Create the Apple-style weather app | |
| apple_weather_app = APPLE_WEATHER_APP() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment