cppscripts/statusweather.cpp

212 lines
6.1 KiB
C++

// Print weather information for i3 status bar.
/* i3blocks config:
* [weather]
* command=statusweather
* interval=persist
* markup=pango
*/
/* ~/.config/statusweather.cfg:
* api_key = abc123
* city = Hamburd,de
*/
#include "helpers.hpp"
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/program_options/variables_map.hpp>
#include <fmt/core.h>
#include <nlohmann/json.hpp>
#include <restclient-cpp/connection.h>
#include <restclient-cpp/restclient.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <fstream>
#include <functional>
#include <future>
#include <iostream>
#include <map>
#include <mutex>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <tuple>
struct weather
{
float temperature{-273.15};
std::string icon{""};
bool old{true};
} __attribute__((aligned(64))) weather; // NOLINT(cert-err58-cpp)
std::mutex mutex_weather;
std::string map_icon(const std::string_view icon_id)
{
// <https://openweathermap.org/weather-conditions>
const std::map<std::uint8_t, std::string> icons{{{1, "🌞"},
{2, ""},
{3, ""},
{4, ""},
{9, "🌧"},
{10, "🌧"},
{10, ""},
{13, "🌨"},
{50, "🌫"}}};
// TODO: Differenciate between day and night.
auto icon{icons.find(std::stoul(icon_id.data()))};
if (icon != icons.end())
{
return icon->second;
}
return {};
}
std::tuple<std::string, std::string> get_options()
{
namespace po = boost::program_options;
po::options_description options("Options");
// clang-format off
options.add_options()
("api_key", po::value<std::string>()->required()->value_name("API key"),
"API key for openweathermap.org.")
("city", po::value<std::string>()->required()->value_name("City"),
"City you want the weather data for.")
;
// clang-format on
po::variables_map vm;
const auto path{[]()
{
auto path{helpers::get_env("XDG_CONFIG_HOME")};
if (path.empty())
{
path = helpers::get_env("HOME");
if (!path.empty())
{
path += "/.config";
}
}
return path += "/statusweather.cfg";
}()};
std::ifstream configfile(path);
po::store(po::parse_config_file(configfile, options, true), vm);
configfile.close();
if ((vm.count("api_key") == 0) || (vm.count("city") == 0))
{
throw std::runtime_error{"api_key or city not configured."};
}
return std::make_tuple(vm["api_key"].as<std::string>(),
vm["city"].as<std::string>());
}
bool fetch_weather()
{
using fmt::format;
std::string api_key;
std::string city;
try
{
std::tie(api_key, city) = get_options();
}
catch (std::runtime_error &e)
{
std::cout << R"(<span color="red">)" << e.what() << "</span>"
<< std::endl;
return false;
}
RestClient::init();
RestClient::Connection conn(
"http://api.openweathermap.org/data/2.5/weather");
conn.FollowRedirects(true, 5);
conn.SetTimeout(10);
// <https://openweathermap.org/current>
auto response{
conn.get(format("?appid={0:s}&q={1:s}&units=metric", api_key, city))};
RestClient::disable();
if (response.code == 200)
{
std::lock_guard<std::mutex> guard(mutex_weather);
const auto json{nlohmann::json::parse(response.body)};
weather.temperature = json[0]["main"]["temp"].get<float>();
weather.icon = map_icon(
json[0]["weather"][0]["icon"].get<std::string_view>());
weather.old = false;
}
else
{
weather.old = true;
}
return true;
}
void print_weather()
{
using fmt::format;
std::lock_guard<std::mutex> guard(mutex_weather);
const std::string color{[]
{
if (weather.temperature > 25.0)
{
return "#ff2200";
}
if (weather.temperature < 0.0)
{
return "#aaffff";
}
if (weather.temperature < 10.0)
{
return "#44ddff";
}
return "#66ff66";
}()};
std::cout << format(R"({0:s} <span color="{1:s}">{2:.1f}°C</span>{3:s})",
weather.icon, color, weather.temperature,
weather.old ? R"( <span color="red">⏳</span>)" : "")
<< std::endl;
}
void update(std::atomic<bool> &cancelled)
{
using clock = std::chrono::system_clock;
using namespace std::chrono_literals;
while (!cancelled)
{
if (fetch_weather())
{
print_weather();
}
std::this_thread::sleep_until(clock::now() + 30min);
}
}
int main()
{
// TODO: Implement clean shutdown.
std::atomic<bool> cancelled{false};
auto future{std::async(std::launch::async, update, std::ref(cancelled))};
std::string line;
while (std::getline(std::cin, line)) // Button click is sent to stdin.
{
if (line == "1") // Left mouse button.
{
if (fetch_weather())
{
print_weather();
}
}
}
}