Weather - a CLI and library for fetching OpenWeatherMap data

Features

  • Access to raw API responses

  • Access to formatted rain reports

  • ANSI-colorized output

  • Detailed rain intensity forecast for the next hour

  • Weather alerts

  • Customizable length and interval for hourly weather

  • Customizable time (12 or 24 hour) and temp display (celsius, fahrenheit, kelvin)

  • Usable as a dependency or standalone CLI

  • Weather lookup by ZIP code and country code

  • Mock weather responses to test responses for varying conditions

This has been my pet project to level up my Elixir skills. Please leave any and all feedback you have (even if it’s just nitpicks about style/code organization, etc.). Thank you!

Just released v0.3.0. Please take a look at the README or the docs.

14 Likes

Just released Version 0.3.1 which introduces… :drum:… customization!

Check out the new Customization section of the README

Customization

How to Customize your Weather Report

  1. Define a module in lib/weather/report/custom that implements the Weather.Report behaviour (definines generate/1).

    defmodule Weather.Report.Custom.FullMoon do
      @moduledoc """
      A custom `Weather.Report` that prints when there's a full moon.
      """
    
      use Weather.Report
    
      @full_moon_phase 0.5
    
      @doc """
      Generates a full moon report.
      """
      @impl Weather.Report
      def generate({report, %{"daily" => [%{"moon_phase" => @full_moon_phase} | _]} = body, opts}) do
        {
          ["🌝🌚 OMG FULL MOON TONIGHT 🌚🌝" | report],
          body,
          opts
        }
      end
    
      def generate(weather), do: weather
    end
    
  2. Add that module to the list of custom reports in your config/config.exs

    config :weather, custom_reports: [Weather.Report.Custom.FullMoon]
    
  3. Re-generate the escript

    $ mix escript.build
    
  4. Wait for a full moon…:new_moon::waxing_crescent_moon::first_quarter_moon::waxing_gibbous_moon::full_moon:

  5. Enjoy your customized weather report

    $ ./weather
    
    🌝🌚 OMG FULL MOON TONIGHT 🌚🌝
    
    🌞 5:17AM | 🌚 8:25PM
    
    76°  ⬇   74°  ⬇   64°  ⬇   60°  ⬇   58°
    3PM      6PM      9PM      12AM     3AM
    
    77° | scattered clouds | 37% humidity
    
  6. Issue a pull request to have your custom report added to the repo so others can use it! :smiley: (please don’t include the changes you made to config/config.exs in your PR)

Would love to see what kind of custom reports you all build! <3

  • Spencer
1 Like

0.3.2

Bug Fixes

  • Fix a bug where the hourly rain report would report rain for the current hour when no more rain was expected between now and the end of the hour (752e374)

Features

An example using the new --color-codes and --feels-like switches:

Check out the Custom Colors and Options sections of the README for more information on the --color-codes switch.

Enjoy!

2 Likes

Hey,

You mention “weather lookup by ZIP code”. This is US only, isn’t it?

It supports non-US zips as well!

The provided value is passed as the zip argument to this OpenWeatherMap Geocoding API call.

Zip/post code and country code divided by comma. Please use ISO 3166 country codes.

I’ll add this info to the README since this isn’t called out. Thanks!

1 Like

Very cool project! Also nice to see how you’ve been working on it the last couple of months (deducing from the commit history). Nice work.

Since you’ve taken the effort to add support running it as an escript, you could consider updating the instructions to install from github (or hex). This worked without any problem for me:

mix escript.install github spencerolson/weather

This installs it somewhere in an escripts folder that can be added to your $PATH. Probably also possible to install from hex (didn’t try). See the escript docs for more options.

One minor remark: the time shows up in the correct time zone when running through the CLI. But when showing the report in Livebook for example, the times are off (they seem to appear in UTC).

I have no idea what to do with this library yet :slight_smile: but it was a nice time fiddling with it, and a good example of how to grow and document such a library/cli-tool.

First of all, thank you for the kind words! It’s been a fun project and I’ve learned so much from the Elixir community (especially on this forum) already.

Since you’ve taken the effort to add support running it as an escript, you could consider updating the instructions to install from github (or hex). This worked without any problem for me:

This is great, thank you. To be honest I did not know an escript could be installed this way. I’ll go ahead and update the README, and I’ll check out to see if it’s possible to install from hex.

As a next step, I’m toying around with moving away from escripts and instead using the burrito library to create self-contained executables. The only snag I’m running into right now is I can’t seem to find any way to provide a code-signed executable for MacOS Gatekeeper without paying for the $99/year developer license.

One minor remark: the time shows up in the correct time zone when running through the CLI. But when showing the report in Livebook for example, the times are off (they seem to appear in UTC).

Good call out! I will update the README with instructions for proper setup. In order to have times returned in the correct time zone, a time zone database needs to be configured for elixir.

In the example of a Livebook, the Mix install command should be:

Mix.install(
  [{:weather, "~> 0.3.3"}],
  config: [elixir: [time_zone_database: Tz.TimeZoneDatabase]]
)

and then you should see the times being reported with the correct zone.

Your mention of a Livebook makes me think I should add one for this project so folks can play around with interactive examples. Thanks for the idea!

1 Like

Version 0.3.4

There’s now a Livebook! Click the button below to give it a try :sun_with_face: :cloud_with_rain: :zap:

Run in Livebook

0.3.4

Features

  • Add a Livebook! And add “Run in Livebook” button to README (8270eee)

  • Improve --zip docs, making it clear non-US codes are accepted (7b104d7)

  • Add timezone database info to readme (db8ae9e)

1 Like

Great, I’ve just started livebook this second.

I get following error:

** (ArgumentError) Invalid --zip. Value provided must be a valid zip code. Received: "30-003,PL"
    (weather 0.3.4) lib/weather/opts.ex:135: anonymous fn/3 in Weather.Opts.new/1
    (elixir 1.17.2) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
    #cell:3uqg3k2cm756vorj:2: (file)

I then tried with

opts =
  Weather.Opts.new(
  zip: 90210,
  name: "Beverly Hills",
  lat: 34.0901,
  lon: -118.4065,
  country: US
  )

and then I get the error message

** (ArgumentError) Missing API key. Please set the OPENWEATHER_API_KEY environment variable or provide a value via the --api-key flag.
    (weather 0.3.4) lib/weather/opts.ex:135: anonymous fn/3 in Weather.Opts.new/1
    (elixir 1.17.2) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
    #cell:3uqg3k2cm756vorj:2: (file)

Hrmmm…that error message may be misleading. That means the lookup by zip code returned a non-200 response. Can you see what you get when you try this in the Livebook?

Weather.API.fetch_location(%{zip: "30-003,PL"}, <your-api-key>)

The Missing API Key repsonse looks expected. If you have set your API Key in the Livebook with the name OPENWEATHER_API_KEY then livebook actually stores it as LB_OPENWEATHER_API_KEY, so within the Livebook you should either:

  1. provide the api_key directly to the Weather.Opts.new/1 call, OR
  2. set the ENV var as directed in the Livebook instructions (which saves it as LB_OPENWEATHER_API_KEY), then create a variable api_key = System.fetch_env!("LB_OPENWEATHER_API_KEY") and use that value for your calls (this is from the step under the Fetching Real Data section in the Livebook)

Update: I released a new version 0.3.5 that includes a fix so that you no longer get a misleading error message about an invalid --zip when the api key is invalid.

That results in

{:ok,
 %Req.Response{
   status: 200,
   headers: %{
     "access-control-allow-credentials" => ["true"],
     "access-control-allow-methods" => ["GET, POST"],
     "access-control-allow-origin" => ["*"],
     "connection" => ["keep-alive"],
     "content-type" => ["application/json; charset=utf-8"],
     "date" => ["Wed, 11 Sep 2024 20:00:29 GMT"],
     "server" => ["openresty"],
     "x-cache-key" => ["/geo/1.0/zip?zip=30-003%2cpl"]
   },
   body: %{
     "country" => "PL",
     "lat" => 50.0833,
     "lon" => 19.9167,
     "name" => "KrakĂłw",
     "zip" => "30-003"
   },
   trailers: %{},
   private: %{}
 }}

This is the response I’d expect, and shouldn’t lead to the Invalid --zip. Value provided must be a valid zip code error you were seeing…perhaps the OpenWeatherMap API was having an intermittent issue during your initial request.

A request like:

opts =
  Weather.Opts.new(
    api_key: api_key,
    zip: "30-003,PL",
    units: "metric",
    twelve: false
  )

{:ok, real_response} = Weather.API.fetch_weather(opts)

should get you some data back, assuming you’ve got a variable api_key assigned to a valid key.

Is it possible that I do not only need to open an account at https://openweathermap.org/, but also have to subscribe afterwards at One Call API 3.0 - OpenWeatherMap (with billing information)?

i now get the response:

%{
  "cod" => 401,
  "message" => "Please note that using One Call 3.0 requires a separate subscription to the One Call by Call plan. Learn more here https://openweathermap.org/price. If you have a valid subscription to the One Call by Call plan, but still receive this error, then please see https://openweathermap.org/faq#error401 for more info."
}```

Sorry for derailing your thread.

No worries, I appreciate you giving it a try.

Yes, you need to subscribe to the One Call API 3.0, however you can set a limit to ensure you never go over the free 1000 calls per day. There is a bit more detail in the Creating an API Key section on the README. Let me know if you have any other questions!

In the meantime, you can play around with fake weather data using the test option passed to Weather.Opts.new/1. you can pass test: "storm", test: "rain", or test: "clear".

2 Likes