201 lines
5.6 KiB
C++
201 lines
5.6 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 = Hamburg,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 <exception>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <future>
|
|
#include <iostream>
|
|
#include <locale>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <tuple>
|
|
#include <utility>
|
|
|
|
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>
|
|
// NOTE: There does not seem to be weather emojis with the moon in unicode.
|
|
const std::map<std::uint8_t, std::pair<std::string, std::string>> icons{
|
|
{{1, {"🌞", "🌝"}}, // clear sky
|
|
{2, {"🌤", "🌤"}}, // few clouds
|
|
{3, {"⛅", "⛅"}}, // scattered clouds
|
|
{4, {"☁", "☁"}}, // broken clouds
|
|
{9, {"🌧", "🌧"}}, // shower rain
|
|
{10, {"🌧", "🌧"}}, // rain
|
|
{10, {"🌩", "🌩"}}, // thunderstorm
|
|
{13, {"🌨", "🌨"}}, // snow
|
|
{50, {"🌫", "🌫"}}}
|
|
}; // mist
|
|
const auto icon{icons.find(std::stoul(icon_id.data()))};
|
|
|
|
if (icon != icons.end()) {
|
|
if (*icon_id.rbegin() == 'n') // Night
|
|
{
|
|
return icon->second.second;
|
|
}
|
|
return icon->second.first;
|
|
}
|
|
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;
|
|
std::ifstream configfile(
|
|
helpers::get_config_file_path("statusweather.cfg"));
|
|
po::store(po::parse_config_file(configfile, options, true), vm);
|
|
configfile.close();
|
|
|
|
po::notify(vm);
|
|
|
|
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(120);
|
|
// <https://openweathermap.org/current>
|
|
auto response{
|
|
conn.get(format("?appid={0:s}&q={1:s}&units=metric", api_key, city))};
|
|
RestClient::disable();
|
|
|
|
const std::lock_guard<std::mutex> guard(mutex_weather);
|
|
if (response.code == 200) {
|
|
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;
|
|
|
|
const 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"(<big>{0:s}</big> )"
|
|
R"(<span color="{1:s}">{2:.1Lf}°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() {
|
|
try {
|
|
std::locale::global(std::locale(""));
|
|
// 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();
|
|
}
|
|
}
|
|
}
|
|
} catch (const std::exception &e) {
|
|
std::cout << R"(<span color="red">)" << e.what() << "</span>"
|
|
<< std::endl;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Local Variables:
|
|
// eval: (rainbow-mode 1)
|
|
// End:
|