Building a Julia-Powered E-Ink Dashboard: A Dev Log

Log 04: July 20th, 2025 - Polar Pi :pie:

Behold! We have OpenMeteo.jl now! It is undocumented, untested, and unregistered but it exists as a package now and fully utilizes the FlatBuffer specification from OpenMeteo! It supports all endpoints from OpenMeteo including the forecast, air quality, and additional modules here: 🌦️ Docs | Open-Meteo.com

It only has one function right now but here is an example on how to use it:

using OpenMeteo

url = "https://api.open-meteo.com/v1/forecast"
params = Dict(
    "latitude" => "42.3751",
    "longitude" => "-71.1056",
    "daily" => [
        "temperature_2m_max",
        "temperature_2m_min",
        "sunset",
        "sunrise",
        "wind_speed_10m_max",
        "wind_gusts_10m_max",
        "wind_speed_10m_min",
        "wind_gusts_10m_min"
    ],
    "hourly" => [
        "temperature_2m",
        "uv_index",
        "precipitation_probability",
        "precipitation",
        "relative_humidity_2m",
        "wind_speed_10m",
        "wind_direction_10m"
    ],
    "timezone" => "America/New_York",
    "forecast_days" => "1",
    "timeformat" => "unixtime",
    "wind_speed_unit" => "mph",
    "temperature_unit" => "fahrenheit",
    "precipitation_unit" => "inch"
)
method = "GET"
verify = true

resp = OpenMeteo.request_weather_api(url, params = params, method = method, verify = verify)[1]

hourly_vars = Dict(params["hourly"] .=> [resp.hourly.variables[x].values for x in 1:length(params["hourly"])])
daily_vars = Dict(params["daily"] .=> [resp.daily.variables[x].values for x in 1:length(params["daily"])])
sunset = resp.daily.variables[3].values_int64
sunrise = resp.daily.variables[4].values_int64

Which gives an output response like:

julia> resp
OpenMeteo.WeatherApiResponse{OpenMeteo.VariablesWithTime{OpenMeteo.VariableWithValues}, OpenMeteo.VariablesWithTime{OpenMeteo.VariableWithValues}, OpenMeteo.Vari
ablesWithTime{OpenMeteo.VariableWithValues}, OpenMeteo.VariablesWithTime{OpenMeteo.VariableWithValues}, OpenMeteo.VariablesWithTime{OpenMeteo.VariableWithValues}
}
  latitude: Float32 42.372776f0
  longitude: Float32 -71.09675f0
  elevation: Float32 12.0f0
  generation_time_milliseconds: Float32 0.106573105f0
  location_id: Int64 0
  model: OpenMeteo.Model OpenMeteo.Modelbest_match
  utc_offset_seconds: Int32 -14400
  timezone: String "America/New_York"
  timezone_abbreviation: String "GMT-4"
  current: Nothing nothing
  daily: OpenMeteo.VariablesWithTime{OpenMeteo.VariableWithValues}
  hourly: OpenMeteo.VariablesWithTime{OpenMeteo.VariableWithValues}
  minutely_15: Nothing nothing
  six_hourly: Nothing nothing

On this occasion, I celebrate with a gif:

So what do I do with this unstoppable power? Well, visualize it of course!


Here are some rough cut attempts at the moment:

using CairoMakie
using OpenMeteo

url = "https://api.open-meteo.com/v1/forecast"
params = Dict(
    "latitude" => "42.3751",
    "longitude" => "-71.1056",
    "daily" => [
        "temperature_2m_max",
        "temperature_2m_min",
        "sunset",
        "sunrise",
        "wind_speed_10m_max",
        "wind_gusts_10m_max",
        "wind_speed_10m_min",
        "wind_gusts_10m_min"
    ],
    "hourly" => [
        "temperature_2m",
        "uv_index",
        "precipitation_probability",
        "precipitation",
        "relative_humidity_2m",
        "wind_speed_10m",
        "wind_direction_10m"
    ],
    "timezone" => "America/New_York",
    "forecast_days" => "1",
    "timeformat" => "unixtime",
    "wind_speed_unit" => "mph",
    "temperature_unit" => "fahrenheit",
    "precipitation_unit" => "inch"
)
method = "GET"
verify = true

resp = OpenMeteo.request_weather_api(url, params = params, method = method, verify = verify)[1]

hourly_vars = Dict(params["hourly"] .=> [resp.hourly.variables[x].values for x in 1:length(params["hourly"])])
daily_vars = Dict(params["daily"] .=> [resp.daily.variables[x].values for x in 1:length(params["daily"])])
sunset = resp.daily.variables[3].values_int64
sunrise = resp.daily.variables[4].values_int64

f = Figure(size = (400, 240));

ax = PolarAxis(
    f[1, 1], 
    rminorgridvisible = false,
    rminorticksvisible = false,
    rgridvisible = false,
    rticks = LinearTicks(2),
    rticklabelsize = 10,
    rticklabelsvisible = false,
    rgridcolor = :blue,
    rlimits = (minimum(hourly_vars["temperature_2m"]) - 5, maximum(hourly_vars["temperature_2m"]) + 5),
    thetaminorticksvisible = false,
    thetaticks = (collect(-3pi/2:15 * pi / 180:pi/2)[1:end-1], vcat("0", ["$x" for x in 23:-1:1])),
    thetaticklabelsize = 10,
    thetaminorticks = IntervalsBetween(3),
    thetagridcolor = :lightgray
)

lines!(ax, collect(-3pi/2:15 * pi / 180:pi/2), vcat(reverse(hourly_vars["temperature_2m"]), hourly_vars["temperature_2m"][1]), color = :dimgray)

markers = []
for val in reverse(hourly_vars["uv_index"])
    println(val)
    if 0 <= val < 3
        push!(markers, :circle)
    elseif 3 <= val < 6
        push!(markers, :utriangle)
    elseif 6 <= val < 8
        push!(markers, :diamond)
    elseif 8 <= val < 11
        push!(markers, :star4)
    elseif 11 <= val 
        push!(markers, :xcross)
    end
end

scatter!(ax, collect(-3pi/2:15 * pi / 180:pi/2)[1:end-1], reverse(hourly_vars["temperature_2m"]), color = :black, markersize = 12, marker = markers) 

Which gives the following:

The symbols correspond to UV index (with circle being least dangerous and xcross being the most dangerous) and this represents temperature change over 24 hours. I still need a better way to display the band minimum and maximum weather temperatures. Don’t know an elegant way yet…

From there, we have windspeed and direction!

f = Figure(size = (400, 240));

ax = PolarAxis(
    f[1, 1], 
    rminorgridvisible = false,
    rminorticksvisible = false,
    rgridvisible = false,
    rticks = LinearTicks(2),
    rticklabelsize = 10,
    rticklabelsvisible = true,
    rgridcolor = :blue,
    rlimits = (minimum(hourly_vars["wind_speed_10m"]), maximum(hourly_vars["wind_speed_10m"]) + 2),
    thetaminorticksvisible = false,
    thetaticks = (collect(-3pi/2:15 * pi / 180:pi/2)[1:end-1], vcat("0", ["$x" for x in 23:-1:1])),
    thetaticklabelsize = 10,
    thetaminorticks = IntervalsBetween(3),
    thetagridcolor = :lightgray
)

lines!(ax, collect(-3pi/2:15 * pi / 180:pi/2), vcat(reverse(hourly_vars["wind_speed_10m"]), hourly_vars["wind_speed_10m"][1]), color = :dimgray)
scatter!(ax, collect(-3pi/2:15 * pi / 180:pi/2)[1:end-1], reverse(hourly_vars["wind_speed_10m"]), color = :black, markersize = 12, marker = '↑', rotation = hourly_vars["wind_direction_10m"] .* (pi / 180))

It’s not quite so clean yet, but we are getting there.


Now, I need to figure out how to plot a radial bar chart with probabilities of precipitation and precipitation amount. Additionally, I have no idea how to make this into one figure that shares concentric rings. Ideally, windspeed would be inside the temperature and UV plot. Does anyone know how to do this?

At any rate, we are progressing quite well! Until the next adventurous hacking – maybe at JuliaCon with @mitiemannn? :thinking: I’ll be bringing my Pi and display with me. :smiley: Happy to hack when not engaged with JuliaHealth shenanigans or AlgebraicJulia stuff too! Just grab me!

~ tcp :deciduous_tree:

2 Likes