Skip to content

Instantly share code, notes, and snippets.

@Staars
Created October 7, 2025 19:55
Show Gist options
  • Select an option

  • Save Staars/93a4f53d980777a7307f9ee7369da12a to your computer and use it in GitHub Desktop.

Select an option

Save Staars/93a4f53d980777a7307f9ee7369da12a to your computer and use it in GitHub Desktop.
Apple Style Weather App
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}&current=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