A daily cronjob runs a Haskell script in IHP which gets weather forecast data from the OpenWeather One Call API, checks for temperatures below freezing point, and, in case of frost, sends a warning via a Discord bot.
#!/usr/bin/env run-script module Application.Script.WeatherApiFrostwarning where import Application.Script.Prelude hiding (get) import Network.Wreq import Control.Lens ((^?), (^..), folded) import Data.Aeson import GHC.Generics import Data.Aeson.Lens (key, values) import qualified Data.Text as T import qualified Application.Helper.Script as H (postToDiscord) import Web.Controller.Prelude (Value(Number)) import Data.Scientific (Scientific)
We use the HTTP client library Wreq to get the data, which uses
Lens. We hide
Prelude to use
Wreq‘s. The message for discord is JSON encoded with Aeson. The Discord client code is placed in a module
Application.Helper.Script to handle this in line with the corresponding
Controller helpers, which are available app-wide.
The main function to run the script
This function can be run locally with
%> Application/Script/WeatherApiFrostwarning.hs, it should be placed in IHP’s script directory. In case your IHP is hosted on IHP Cloud, you can select it on the Settings page for a cronjob. I’m running it daily at morning time to have the whole day to prepare. If you run it hourly, you’ll get spammed in case of frost ahead…
run :: Script run = do minTemp24 <- getMinTempNext24Hours openWeatherUrl if minTemp24 < Number 0 then H.postToDiscord (postBodyFrostwarning minTemp24)
We call a function to get the lowest temperature for the next 24 hours, and we call our Discord bot in case of frost, passing along the temperature value.
This is the URL for my location; see OpenWeather Docs.
openWeatherUrl :: String openWeatherUrl = "https://api.openweathermap.org/data/2.5/onecall?lat=52.00738778924009&lon=8.823055903576584&exclude=current,minutely,daily,alerts&appid=a44bf38a181fb428e16ab64d0639a2ba&units=metric&lang=de"
Accessing the response and converting to a Haskell value
Wreq is thin. I didn’t find much apart from the the author’s own tutorial. Chris Penner’s interesting post Generalizing ‘Jq’ And Traversal Systems Using Optics And Standard Monads, which isn’t even about
Wreq, helped me to get it done.
getMinTempNext24Hours :: String -> IO Value getMinTempNext24Hours url = do r <- get openWeatherUrl let minTemp24 = r ^? responseBody . key "hourly" ^..folded . values . key "temp" |> take 24 |> minimum pure minTemp24
Without digging into
Lens which I’m not even beginning to get, the response
r is here accessed by stepping inwards along the lenses wreq produces from the JSON response. If you want to get a feeling for it, try it step by step in GHCI. After importing the module, you still, for some reason, have to add
Network.Wreq. in front of the
Sticking to IHP’s pipe style with its
|> operator, we can apply
take 24 (for we don’t want two days, but only one day = 24 hourly dates forecast) and
minimum to the resulting number. The function gives back the min temperatur with type
Value; comparing it to
Number 0 in the
run function works anyway.
Composing the warning message
postBodyFrostwarning temp = object [ "content" .= T.unpack "<@&805016503044276236> **NIGHT FROST** " ++ T.unpack temp_info ++ T.unpack " degrees" ] where Number temp_nr = temp temp_info = show temp_nr
We need to convert
String for Aeson’s
.= operator, which makes JSON key value pairs. From
temp, which is of type
Value, we need to extract
Text before, which I do in a plump manner.
The Discord bot
Finally, we want to post our message to Discord. Again, we use
module Application.Helper.Script where import Application.Script.Prelude import qualified Network.Wreq as Wreq import qualified Control.Lens as Lens((^.), (.~), (&)) import qualified Data.Map as Map import qualified Data.Aeson as Aeson import qualified Data.Aeson.Lens as L import GHC.Generics postToDiscord body = do let opts = Wreq.defaults Lens.& Wreq.header "Authorization" Lens..~ ["Bot <your_bot_token>"] r <- Wreq.postWith opts "https://discord.com/api/webhooks/<your_client_id>/\ \<your_discord_channel_webhook>" (Aeson.toJSON body) let datas = r Lens.^. Wreq.responseBody . L.key "data" . L._String ua = r Lens.^. Wreq.responseBody . L.key "headers" . L.key "User-Agent" . L._String ganzR = r Lens.^. Wreq.responseBody . L._String
The webhook id is the last part of the webhook URL from the integrations panel of your Discord channel, coming after the client id. The Wreq tutorial introduces the use of the
header lens in order to authorize with Discord.