5. Fledex: Weather example
Mix.install([
{:fledex, "~>0.5"},
{:jason, "~>1.2"}
])
# we define a couple of aliases to not have to type that much
alias Fledex.Animation.Manager
alias Fledex.Leds
alias Fledex.LedStrip
alias Fledex.Driver.Impl.Kino
alias Fledex.Color.Correction
alias Fledex.Utils.PubSub
{:ok, pid} = Manager.start_link()
:ok
Intro
In the last project we created a clock. We could make a call to get the current time in every redraw loop. This approach is not possible for data that we are fetching from an external source. In this example we will create a weather display. We will get the information from an API.
If we would take the same approach as in the clock example, we would make an API call every 500ms which probably will result in us getting blocked. It is completely unnecessary to make that many call, since the wheather is not changing that frequently.
What we will do is to check every 15 min what the latest weather news is.
There are plenty of Weather APIs on the internet, but most of them require to register which is not ideal for a public example. Therefore I decided to use Open-Meteo which provides all we need.
We need to decide for which location we want to get the weather. We use the coordinate for Budapest (47.4729344
,19.0468802
). Then we can make the call https://api.open-meteo.com/v1/forecast?latitude=47.4729344&longitude=19.0468802¤t=temperature_2m
to get data like the following:
{
"latitude": 47.5,
"longitude": 19.0625,
"generationtime_ms": 0.04100799560546875,
"utc_offset_seconds": 0,
"timezone": "GMT",
"timezone_abbreviation": "GMT",
"elevation": 113,
"current_units": {
"time": "iso8601",
"interval": "seconds",
"temperature_2m": "°C"
},
"current": {
"time": "2023-10-30T13:30",
"interval": 900,
"temperature_2m": 22.1
}
}
As can be seen we get the information we are interested in, the current temperature (in °C)
Configuring the led strip
Now it’s time to prepare our led strip. First we define our strip and we set a very low refresh rate. An update every 10 sec is enough.
# we start with the default configuration, but change the timer update frequency to 10 sec
:ok = Manager.register_strip(:weather, [{Kino, []}], timer_update_timeout: 10_000)
:ok
Now we define two animations, the first one creates blue (below zero), green (zero), or red (above zero) indicators. The indicators have the length of the temperature and start from zero to either go to the left (negative) or the right (positive).
The temperature will be delivered to us as part of the trigger. We’ll look at how we will get this information in the section below.
As and additional bonus, the thermometer is showing the full scale, if no proper measurement is received (arbitrarily high value of 100).
Manager.register_config(:weather, %{
temperature: %{
type: :animation,
def_func: fn triggers ->
temp = triggers[:temperature] || 100
temp = round(temp)
temp =
if temp == 100 do
100
else
temp = min(temp, 40)
max(temp, -20)
end
{leds, offset} =
case temp do
temp when temp == 100 ->
{Leds.leds(61)
|> Leds.light(:blue, 1, 20)
|> Leds.light(:may_green)
|> Leds.light(:red, 22, 40), 1}
temp when temp < 0 ->
{Leds.leds(1) |> Leds.light(:blue) |> Leds.repeat(-temp), 21 + temp}
temp when temp == 0 ->
{Leds.leds(1) |> Leds.light(:may_green), 21}
temp when temp > 0 ->
{Leds.leds(1) |> Leds.light(:red) |> Leds.repeat(temp), 22}
end
Leds.leds(61) |> Leds.light(leds, offset)
end
},
help: %{
type: :animation,
# we allow temperatures down to -20C and up to +40C
def_func: fn _triggers ->
negative_scale =
Leds.leds(5)
|> Leds.light(:ash_gray)
|> Leds.repeat(4)
positive_scale =
Leds.leds(5)
|> Leds.light(:ash_gray, 5)
|> Leds.repeat(8)
Leds.leds(61)
|> Leds.light(negative_scale)
|> Leds.light(:ash_gray)
|> Leds.light(positive_scale)
end
}
})
For testing we can send an arbitrary temperature value to our strip in the following way. This allows us to also see whether the strip has the correct behavior when the temperature is at zero (or even below) degree. It wouldn’t be really practical if we need to wait until the outside temperature reaches those levels.
Here we send a temperature value of -15.2 degree (which the strip should round to -15 degree)
PubSub.broadcast_trigger(%{temperature: -15.2})
Getting the weather data
Before we can start with the weather data, we need to make sure that we can make http client calls (also over https) and therefore we start the two services.
:inets.start()
:ssl.start()
Now we can make the actual call. We do want to make this call in regular intervals (every 15 min), parse the response and publish the temperature through pubsub. To do this we create a small server. We start it with our URL (note, the parsing in the server is URL specific, so you can’t simply change it to another site) and with our 15min refresh interval (specified in ms).
The server will extract the temperature and send it via pubsub to our update functions.
defmodule CallUrlPeriodic do
use GenServer
def start_link(url, interval_ms) do
pid = Process.whereis(__MODULE__)
if pid == nil do
GenServer.start_link(__MODULE__, %{url: url, interval_ms: interval_ms}, name: __MODULE__)
else
# server is already running. We could reconfigure it, but we don't do this here.
{:ok, pid}
end
end
@impl true
def init(state) do
:timer.send_interval(state.interval_ms, :work)
# we send ourselve immediately a request
send(self(), :work)
{:ok, state}
end
@impl true
def handle_info(:work, state) do
{:ok, resp} =
:httpc.request(:get, {String.to_charlist(state.url), []}, [], body_format: :binary)
{{_, 200, ~c"OK"}, _headers, body} = resp
json = Jason.decode!(body)
temperature = json["current"]["temperature_2m"]
PubSub.broadcast_trigger(%{temperature: temperature})
{:noreply, state}
end
end
{:ok, pid} =
CallUrlPeriodic.start_link(
"https://api.open-meteo.com/v1/forecast?latitude=47.4729344&longitude=19.0468802¤t=temperature_2m",
900_000
)