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 took the same approach as in the clock example, we would make an API call every 500ms which probably would result in us getting blocked. It is completely unnecessary to make that many call; the wheather doesn’t change that frequently.
What we will do is to check every 15 min what the latest weather status 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. Since I’m living in Budapest I use it’s coordinates (lat: 47.4729344
, lon: 19.0468802
). To figure out your own coordinates, you can use Open Street Map. Search for your location, right click on the exact position, right-click and select Query feature. Now the URL will privide you with the lat
-itude and lon
-gitude coordinates.
Then we can make the call to https://api.open-meteo.com/v1/forecast?latitude=47.4729344&longitude=19.0468802¤t=temperature_2m
to get the weather data that will look 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 second animation is a helper that creates some lines every 5°C to make it easier to read the temperature.
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), which can happen during the initialization phase.
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, offset: 1, repeat: 20)
|> Leds.light(:green)
|> Leds.light(:red, offset: 22, repeat: 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: 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, offset: 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. This allows us to also see whether the strip has the correct behavior when the temperature is at zero (or even below) degree. For testing it would be really unpractical 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). Remember, we set the strip to update slowly and therefore it might (depending on when the next refresh will happen) take a little while before you see the change.
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. In a later chapter we will see how we can use one of the Fledex
services to do the scheduling.
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
)
Annotations
If you want to be more fancy in your display, you could also add other features, like the general weather condition (called weather_code
). For this the wmo codes and their meaning might come in handy.
Currently I see:
...
"current":{
...
"weather_code":3
}
...
meaning that “Clouds generally forming or developing”
Now it’s up to you to become creative. Happy experimenting :-)