diff --git a/CMakeLists.txt b/CMakeLists.txt index 1eb0252..ad2bd2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,11 @@ target_sources(bar_lib src/app.cpp src/bar/bar.cpp + src/connection/httpConnection.cpp + src/connection/dbus/notification.cpp + src/connection/dbus/mpris.cpp + src/connection/dbus/tray.cpp + src/widgets/clock.cpp src/widgets/date.cpp src/widgets/notification/baseNotification.cpp @@ -45,14 +50,12 @@ target_sources(bar_lib src/widgets/notification/notificationWindow.cpp src/widgets/notification/spotifyNotification.cpp src/widgets/volumeWidget.cpp + src/widgets/weather.cpp src/widgets/webWidget.cpp src/services/hyprland.cpp src/services/notificationController.cpp src/services/textureCache.cpp - src/services/tray.cpp - src/services/dbus/notification.cpp - src/services/dbus/mpris.cpp src/widgets/tray.cpp src/widgets/controlCenter/controlCenter.cpp diff --git a/include/app.hpp b/include/app.hpp index 2da1894..2dea63c 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -4,7 +4,8 @@ #include "bar/bar.hpp" #include "services/hyprland.hpp" -#include "services/dbus/notification.hpp" +#include "connection/dbus/notification.hpp" +#include "connection/dbus/tray.hpp" #include "glibmm/refptr.h" #include "gtkmm/application.h" diff --git a/include/connection/dbus/dbus.hpp b/include/connection/dbus/dbus.hpp new file mode 100644 index 0000000..6d52f7f --- /dev/null +++ b/include/connection/dbus/dbus.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +class DbusConnection { + public: + virtual ~DbusConnection() = default; + + protected: + Glib::RefPtr connection; + + void connect_session_async(const sigc::slot &)> &callback) { + Gio::DBus::Connection::get(Gio::DBus::BusType::SESSION, callback); + } + + static void ensure_gio_init() { + try { + Gio::init(); + } catch (const Glib::Error &) { + // Already initialized. + } + } +}; diff --git a/include/services/dbus/messages.hpp b/include/connection/dbus/messages.hpp similarity index 99% rename from include/services/dbus/messages.hpp rename to include/connection/dbus/messages.hpp index 0ea41d6..978fb05 100644 --- a/include/services/dbus/messages.hpp +++ b/include/connection/dbus/messages.hpp @@ -10,8 +10,6 @@ #include "gdkmm/pixbuf.h" #include "glibmm/variant.h" - - struct MprisPlayer2Message { std::string title; std::vector artist; diff --git a/include/services/dbus/mpris.hpp b/include/connection/dbus/mpris.hpp similarity index 95% rename from include/services/dbus/mpris.hpp rename to include/connection/dbus/mpris.hpp index ff21fa6..55560e9 100644 --- a/include/services/dbus/mpris.hpp +++ b/include/connection/dbus/mpris.hpp @@ -5,9 +5,10 @@ #include #include #include -#include "services/dbus/messages.hpp" +#include "connection/dbus/dbus.hpp" +#include "connection/dbus/messages.hpp" -class MprisController { +class MprisController : public DbusConnection { public: struct PlayerState { std::string title; @@ -51,7 +52,6 @@ class MprisController { PlaybackStatus currentPlaybackStatus = PlaybackStatus::Stopped; - Glib::RefPtr m_connection; Glib::RefPtr m_proxy; Glib::RefPtr m_dbus_proxy; std::string m_player_bus_name = "org.mpris.MediaPlayer2.spotify"; diff --git a/include/services/dbus/notification.hpp b/include/connection/dbus/notification.hpp similarity index 96% rename from include/services/dbus/notification.hpp rename to include/connection/dbus/notification.hpp index 88c1878..ec7f4d1 100644 --- a/include/services/dbus/notification.hpp +++ b/include/connection/dbus/notification.hpp @@ -5,6 +5,7 @@ #include #include +#include "connection/dbus/dbus.hpp" #include "giomm/dbusconnection.h" #include "giomm/dbusownname.h" #include "glib.h" @@ -39,7 +40,7 @@ const Glib::ustring introspection_xml = R"( )"; -class NotificationService { +class NotificationService : public DbusConnection { public: NotificationService() : notificationIdCounter(1) { Gio::DBus::own_name( diff --git a/include/services/tray.hpp b/include/connection/dbus/tray.hpp similarity index 98% rename from include/services/tray.hpp rename to include/connection/dbus/tray.hpp index 60cdd9a..79d0d2b 100644 --- a/include/services/tray.hpp +++ b/include/connection/dbus/tray.hpp @@ -15,7 +15,9 @@ #include #include -class TrayService { +#include "connection/dbus/dbus.hpp" + +class TrayService : public DbusConnection { inline static TrayService *instance = nullptr; public: struct Item { @@ -85,7 +87,6 @@ class TrayService { bool addSignalPending = false; }; - Glib::RefPtr connection; Glib::RefPtr nodeInfo; Gio::DBus::InterfaceVTable vtable; diff --git a/include/connection/httpConnection.hpp b/include/connection/httpConnection.hpp new file mode 100644 index 0000000..dc3d0c5 --- /dev/null +++ b/include/connection/httpConnection.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +struct HttpResponse { + long status_code = 0; + std::string body; + std::map headers; + std::string error; + + bool ok() const { + return error.empty(); + } +}; + +class HttpConnection { + public: + static HttpResponse get(const std::string &url, + const std::map &headers = {}, + long timeout_ms = 0); + + static HttpResponse post(const std::string &url, + const std::string &body, + const std::map &headers = {}, + const std::string &content_type = "application/json", + long timeout_ms = 0); + + private: + static HttpResponse performRequest(const std::string &method, + const std::string &url, + const std::string &body, + const std::map &headers, + const std::string &content_type, + long timeout_ms); +}; \ No newline at end of file diff --git a/include/services/notificationController.hpp b/include/services/notificationController.hpp index 7aec2af..0b8a26f 100644 --- a/include/services/notificationController.hpp +++ b/include/services/notificationController.hpp @@ -6,7 +6,7 @@ #include #include -#include "services/dbus/messages.hpp" +#include "connection/dbus/messages.hpp" #include "widgets/notification/baseNotification.hpp" #include "gdkmm/monitor.h" diff --git a/include/widgets/controlCenter/controlCenter.hpp b/include/widgets/controlCenter/controlCenter.hpp index 4045ea7..063d130 100644 --- a/include/widgets/controlCenter/controlCenter.hpp +++ b/include/widgets/controlCenter/controlCenter.hpp @@ -6,6 +6,7 @@ #include "gtkmm/label.h" #include "gtkmm/stack.h" #include "widgets/controlCenter/mediaControl.hpp" +#include "widgets/weather.hpp" #include @@ -18,7 +19,7 @@ class ControlCenter : public Popover { Gtk::Box tabRow; Gtk::Stack contentStack; Gtk::Box controlCenterContainer; - Gtk::Label testLabel; + WeatherWidget weatherWidget; Gtk::Button mediaControl; Gtk::Button testTabButton; std::shared_ptr mprisController = MprisController::getInstance(); diff --git a/include/widgets/controlCenter/mediaControl.hpp b/include/widgets/controlCenter/mediaControl.hpp index 92cc817..2b01c89 100644 --- a/include/widgets/controlCenter/mediaControl.hpp +++ b/include/widgets/controlCenter/mediaControl.hpp @@ -7,7 +7,7 @@ #include "gtkmm/scale.h" #include "gtkmm/scrolledwindow.h" -#include "services/dbus/mpris.hpp" +#include "connection/dbus/mpris.hpp" class MediaControlWidget : public Gtk::Box { public: diff --git a/include/widgets/notification/copyNotification.hpp b/include/widgets/notification/copyNotification.hpp index 959907d..567e022 100644 --- a/include/widgets/notification/copyNotification.hpp +++ b/include/widgets/notification/copyNotification.hpp @@ -2,7 +2,7 @@ #include -#include "services/dbus/messages.hpp" +#include "connection/dbus/messages.hpp" #include "widgets/notification/baseNotification.hpp" #include "gtkmm/box.h" diff --git a/include/widgets/notification/notificationWindow.hpp b/include/widgets/notification/notificationWindow.hpp index 5780259..1b23df0 100644 --- a/include/widgets/notification/notificationWindow.hpp +++ b/include/widgets/notification/notificationWindow.hpp @@ -2,7 +2,7 @@ #pragma once #include -#include "services/dbus/messages.hpp" +#include "connection/dbus/messages.hpp" #include "widgets/notification/baseNotification.hpp" class NotificationWindow : public BaseNotification { diff --git a/include/widgets/notification/spotifyNotification.hpp b/include/widgets/notification/spotifyNotification.hpp index 1a885b4..a656ea5 100644 --- a/include/widgets/notification/spotifyNotification.hpp +++ b/include/widgets/notification/spotifyNotification.hpp @@ -3,7 +3,7 @@ #include #include -#include "services/dbus/messages.hpp" +#include "connection/dbus/messages.hpp" #include "widgets/notification/baseNotification.hpp" #include "gtkmm/centerbox.h" diff --git a/include/widgets/tray.hpp b/include/widgets/tray.hpp index 08cd265..3ae2327 100644 --- a/include/widgets/tray.hpp +++ b/include/widgets/tray.hpp @@ -17,7 +17,7 @@ #include #include -#include "services/tray.hpp" +#include "connection/dbus/tray.hpp" #include "components/base/button.hpp" class TrayIconWidget : public Button { diff --git a/include/widgets/weather.hpp b/include/widgets/weather.hpp new file mode 100644 index 0000000..af0f76a --- /dev/null +++ b/include/widgets/weather.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +class WeatherWidget : public Gtk::Box { + public: + WeatherWidget(); + + private: + Gtk::Label titleLabel; + Gtk::Label currentLabel; + Gtk::Label todayLabel; + + bool onRefreshTick(); + void fetchWeather(); + void applyWeatherText(const std::string ¤t_text, + const std::string &today_text); +}; diff --git a/src/app.cpp b/src/app.cpp index fbb62fb..aa548c9 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -2,8 +2,8 @@ #include #include -#include "services/dbus/notification.hpp" -#include "services/dbus/mpris.hpp" +#include "connection/dbus/notification.hpp" +#include "connection/dbus/mpris.hpp" #include "services/notificationController.hpp" #include "services/textureCache.hpp" diff --git a/src/connection/dbus/dbus.cpp b/src/connection/dbus/dbus.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/services/dbus/mpris.cpp b/src/connection/dbus/mpris.cpp similarity index 96% rename from src/services/dbus/mpris.cpp rename to src/connection/dbus/mpris.cpp index 7fb64a5..1f93d63 100644 --- a/src/services/dbus/mpris.cpp +++ b/src/connection/dbus/mpris.cpp @@ -1,4 +1,4 @@ -#include "services/dbus/mpris.hpp" +#include "connection/dbus/mpris.hpp" #include #include @@ -19,16 +19,12 @@ std::shared_ptr MprisController::createForPlayer(const std::str } MprisController::MprisController() { - Gio::DBus::Connection::get( - Gio::DBus::BusType::SESSION, - sigc::mem_fun(*this, &MprisController::on_bus_connected)); + connect_session_async(sigc::mem_fun(*this, &MprisController::on_bus_connected)); } MprisController::MprisController(const std::string &bus_name) : m_player_bus_name(bus_name) { - Gio::DBus::Connection::get( - Gio::DBus::BusType::SESSION, - sigc::mem_fun(*this, &MprisController::on_bus_connected)); + connect_session_async(sigc::mem_fun(*this, &MprisController::on_bus_connected)); } std::vector MprisController::get_registered_players() const { @@ -65,11 +61,11 @@ void MprisController::on_bus_connected(const Glib::RefPtr &res return; } try { - m_connection = Gio::DBus::Connection::get_finish(result); + connection = Gio::DBus::Connection::get_finish(result); if (!m_dbus_proxy) { m_dbus_proxy = Gio::DBus::Proxy::create_sync( - m_connection, + connection, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus"); @@ -131,10 +127,10 @@ void MprisController::handle_player_registered(const std::string &bus_name) { registeredPlayers.insert(bus_name); - if (bus_name == m_player_bus_name && !m_proxy && m_connection) { + if (bus_name == m_player_bus_name && !m_proxy && connection) { try { m_proxy = Gio::DBus::Proxy::create_sync( - m_connection, + connection, m_player_bus_name, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player"); diff --git a/src/services/dbus/notification.cpp b/src/connection/dbus/notification.cpp similarity index 98% rename from src/services/dbus/notification.cpp rename to src/connection/dbus/notification.cpp index 69d6424..2b862c1 100644 --- a/src/services/dbus/notification.cpp +++ b/src/connection/dbus/notification.cpp @@ -1,11 +1,9 @@ -#include "services/dbus/notification.hpp" +#include "connection/dbus/notification.hpp" #include #include -#include "helpers/string.hpp" #include "services/notificationController.hpp" -#include "widgets/notification/copyNotification.hpp" #include "glib.h" #include "glibconfig.h" diff --git a/src/services/tray.cpp b/src/connection/dbus/tray.cpp similarity index 99% rename from src/services/tray.cpp rename to src/connection/dbus/tray.cpp index 9355b4d..7f8a6ad 100644 --- a/src/services/tray.cpp +++ b/src/connection/dbus/tray.cpp @@ -1,4 +1,4 @@ -#include "services/tray.hpp" +#include "connection/dbus/tray.hpp" #include #include @@ -277,11 +277,7 @@ void TrayService::start() { return; } - try { - Gio::init(); - } catch (const Glib::Error &) { - // Already initialised; ignore. - } + ensure_gio_init(); if (!nodeInfo) { try { diff --git a/src/connection/httpConnection.cpp b/src/connection/httpConnection.cpp new file mode 100644 index 0000000..f8b1a88 --- /dev/null +++ b/src/connection/httpConnection.cpp @@ -0,0 +1,119 @@ +#include "connection/httpConnection.hpp" + +#include +#include +#include +#include +#include + +namespace { +size_t write_to_string(void *contents, size_t size, size_t nmemb, void *userp) { + size_t total = size * nmemb; + auto *buffer = static_cast(userp); + buffer->append(static_cast(contents), total); + return total; +} + +std::string trim(std::string value) { + auto not_space = [](unsigned char c) { return std::isspace(c) == 0; }; + value.erase(value.begin(), + std::find_if(value.begin(), value.end(), not_space)); + value.erase(std::find_if(value.rbegin(), value.rend(), not_space).base(), + value.end()); + return value; +} + +size_t header_to_map(char *buffer, size_t size, size_t nitems, void *userdata) { + size_t total = size * nitems; + auto *header_map = static_cast *>(userdata); + + std::string line(buffer, total); + auto colon = line.find(':'); + if (colon != std::string::npos) { + auto key = trim(line.substr(0, colon)); + auto value = trim(line.substr(colon + 1)); + if (!key.empty()) { + header_map->insert_or_assign(std::move(key), std::move(value)); + } + } + return total; +} +} + +HttpResponse HttpConnection::get(const std::string &url, + const std::map &headers, + long timeout_ms) { + return performRequest("GET", url, std::string(), headers, std::string(), timeout_ms); +} + +HttpResponse HttpConnection::post(const std::string &url, + const std::string &body, + const std::map &headers, + const std::string &content_type, + long timeout_ms) { + return performRequest("POST", url, body, headers, content_type, timeout_ms); +} + +HttpResponse HttpConnection::performRequest(const std::string &method, + const std::string &url, + const std::string &body, + const std::map &headers, + const std::string &content_type, + long timeout_ms) { + HttpResponse response; + + CURL *curl = curl_easy_init(); + if (!curl) { + response.error = "curl_easy_init failed"; + return response; + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_string); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_to_map); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response.headers); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "bar/1.0"); + + if (timeout_ms > 0) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms); + } + + if (method == "POST") { + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); + } else { + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + } + + struct curl_slist *header_list = nullptr; + for (const auto &pair : headers) { + std::string header = pair.first + ": " + pair.second; + header_list = curl_slist_append(header_list, header.c_str()); + } + + if (method == "POST" && !content_type.empty()) { + std::string content_header = "Content-Type: " + content_type; + header_list = curl_slist_append(header_list, content_header.c_str()); + } + + if (header_list) { + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); + } + + auto result = curl_easy_perform(curl); + if (result != CURLE_OK) { + response.error = curl_easy_strerror(result); + } + + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.status_code); + + if (header_list) { + curl_slist_free_all(header_list); + } + curl_easy_cleanup(curl); + + return response; +} diff --git a/src/services/notificationController.cpp b/src/services/notificationController.cpp index edc1933..28afad4 100644 --- a/src/services/notificationController.cpp +++ b/src/services/notificationController.cpp @@ -3,7 +3,7 @@ #include #include -#include "services/dbus/messages.hpp" +#include "connection/dbus/messages.hpp" #include "widgets/notification/baseNotification.hpp" #include "widgets/notification/copyNotification.hpp" #include "widgets/notification/notificationWindow.hpp" diff --git a/src/widgets/controlCenter/controlCenter.cpp b/src/widgets/controlCenter/controlCenter.cpp index 1c0c3a3..31f0f26 100644 --- a/src/widgets/controlCenter/controlCenter.cpp +++ b/src/widgets/controlCenter/controlCenter.cpp @@ -34,10 +34,8 @@ ControlCenter::ControlCenter(std::string icon, std::string name) this->controlCenterContainer.set_orientation(Gtk::Orientation::VERTICAL); this->controlCenterContainer.set_spacing(4); - this->testLabel.set_text("Test tab"); - this->contentStack.add(this->controlCenterContainer, "controls", "Controls"); - this->contentStack.add(this->testLabel, "test", "Test"); + this->contentStack.add(this->weatherWidget, "test", "Test"); this->contentStack.set_visible_child("controls"); this->setActiveTab("controls"); diff --git a/src/widgets/weather.cpp b/src/widgets/weather.cpp new file mode 100644 index 0000000..7f739b0 --- /dev/null +++ b/src/widgets/weather.cpp @@ -0,0 +1,112 @@ +#include "widgets/weather.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr const char *kWeatherUrl = + "https://api.open-meteo.com/v1/forecast?latitude=49.0094&longitude=8.4044&daily=temperature_2m_max,temperature_2m_min,weather_code&hourly=temperature_2m,rain¤t=temperature_2m&timezone=Europe%2FBerlin"; + +size_t write_to_buffer(void *contents, size_t size, size_t nmemb, void *userp) { + size_t total = size * nmemb; + auto *buffer = static_cast(userp); + buffer->append(static_cast(contents), total); + return total; +} + +std::string formatTemp(double value) { + std::ostringstream ss; + ss.setf(std::ios::fixed); + ss.precision(1); + ss << value << "°C"; + return ss.str(); +} +} // namespace + +WeatherWidget::WeatherWidget() + : Gtk::Box(Gtk::Orientation::VERTICAL) { + this->set_orientation(Gtk::Orientation::VERTICAL); + this->set_spacing(6); + this->set_margin_top(4); + this->set_margin_bottom(4); + this->set_margin_start(4); + this->set_margin_end(4); + + this->titleLabel.set_text("Weather"); + this->currentLabel.set_text("Now: --"); + this->todayLabel.set_text("Today: -- / --"); + + this->append(this->titleLabel); + this->append(this->currentLabel); + this->append(this->todayLabel); + + this->fetchWeather(); + Glib::signal_timeout().connect_seconds( + sigc::mem_fun(*this, &WeatherWidget::onRefreshTick), + 3600); +} + +bool WeatherWidget::onRefreshTick() { + this->fetchWeather(); + return true; +} + +void WeatherWidget::fetchWeather() { + std::thread([this]() { + std::string buffer; + + CURL *curl = curl_easy_init(); + if (!curl) { + Glib::signal_idle().connect_once([this]() { + this->applyWeatherText("Now: --", "Today: -- / --"); + }); + return; + } + + curl_easy_setopt(curl, CURLOPT_URL, kWeatherUrl); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_buffer); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "bar/1.0"); + + auto res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + + if (res != CURLE_OK || buffer.empty()) { + Glib::signal_idle().connect_once([this]() { + this->applyWeatherText("Now: --", "Today: -- / --"); + }); + return; + } + + try { + auto json = nlohmann::json::parse(buffer); + double current = json.at("current").at("temperature_2m").get(); + double minTemp = json.at("daily").at("temperature_2m_min").at(0).get(); + double maxTemp = json.at("daily").at("temperature_2m_max").at(0).get(); + + std::string currentText = "Now: " + formatTemp(current); + std::string todayText = "Today: " + formatTemp(minTemp) + " / " + formatTemp(maxTemp); + + Glib::signal_idle().connect_once([this, currentText, todayText]() { + this->applyWeatherText(currentText, todayText); + }); + } catch (const std::exception &ex) { + spdlog::error("Weather parse error: {}", ex.what()); + Glib::signal_idle().connect_once([this]() { + this->applyWeatherText("Now: --", "Today: -- / --"); + }); + } + }).detach(); +} + +void WeatherWidget::applyWeatherText(const std::string ¤t_text, + const std::string &today_text) { + this->currentLabel.set_text(current_text); + this->todayLabel.set_text(today_text); +} diff --git a/weather.json b/weather.json new file mode 100644 index 0000000..7ed9d37 --- /dev/null +++ b/weather.json @@ -0,0 +1,580 @@ +{ + "latitude": 49.0, + "longitude": 8.4, + "generationtime_ms": 0.23317337036132812, + "utc_offset_seconds": 3600, + "timezone": "Europe/Berlin", + "timezone_abbreviation": "GMT+1", + "elevation": 122.0, + "current_units": { + "time": "iso8601", + "interval": "seconds", + "temperature_2m": "°C" + }, + "current": { + "time": "2026-02-03T23:00", + "interval": 900, + "temperature_2m": -0.7 + }, + "hourly_units": { + "time": "iso8601", + "temperature_2m": "°C", + "rain": "mm" + }, + "hourly": { + "time": [ + "2026-02-03T00:00", + "2026-02-03T01:00", + "2026-02-03T02:00", + "2026-02-03T03:00", + "2026-02-03T04:00", + "2026-02-03T05:00", + "2026-02-03T06:00", + "2026-02-03T07:00", + "2026-02-03T08:00", + "2026-02-03T09:00", + "2026-02-03T10:00", + "2026-02-03T11:00", + "2026-02-03T12:00", + "2026-02-03T13:00", + "2026-02-03T14:00", + "2026-02-03T15:00", + "2026-02-03T16:00", + "2026-02-03T17:00", + "2026-02-03T18:00", + "2026-02-03T19:00", + "2026-02-03T20:00", + "2026-02-03T21:00", + "2026-02-03T22:00", + "2026-02-03T23:00", + "2026-02-04T00:00", + "2026-02-04T01:00", + "2026-02-04T02:00", + "2026-02-04T03:00", + "2026-02-04T04:00", + "2026-02-04T05:00", + "2026-02-04T06:00", + "2026-02-04T07:00", + "2026-02-04T08:00", + "2026-02-04T09:00", + "2026-02-04T10:00", + "2026-02-04T11:00", + "2026-02-04T12:00", + "2026-02-04T13:00", + "2026-02-04T14:00", + "2026-02-04T15:00", + "2026-02-04T16:00", + "2026-02-04T17:00", + "2026-02-04T18:00", + "2026-02-04T19:00", + "2026-02-04T20:00", + "2026-02-04T21:00", + "2026-02-04T22:00", + "2026-02-04T23:00", + "2026-02-05T00:00", + "2026-02-05T01:00", + "2026-02-05T02:00", + "2026-02-05T03:00", + "2026-02-05T04:00", + "2026-02-05T05:00", + "2026-02-05T06:00", + "2026-02-05T07:00", + "2026-02-05T08:00", + "2026-02-05T09:00", + "2026-02-05T10:00", + "2026-02-05T11:00", + "2026-02-05T12:00", + "2026-02-05T13:00", + "2026-02-05T14:00", + "2026-02-05T15:00", + "2026-02-05T16:00", + "2026-02-05T17:00", + "2026-02-05T18:00", + "2026-02-05T19:00", + "2026-02-05T20:00", + "2026-02-05T21:00", + "2026-02-05T22:00", + "2026-02-05T23:00", + "2026-02-06T00:00", + "2026-02-06T01:00", + "2026-02-06T02:00", + "2026-02-06T03:00", + "2026-02-06T04:00", + "2026-02-06T05:00", + "2026-02-06T06:00", + "2026-02-06T07:00", + "2026-02-06T08:00", + "2026-02-06T09:00", + "2026-02-06T10:00", + "2026-02-06T11:00", + "2026-02-06T12:00", + "2026-02-06T13:00", + "2026-02-06T14:00", + "2026-02-06T15:00", + "2026-02-06T16:00", + "2026-02-06T17:00", + "2026-02-06T18:00", + "2026-02-06T19:00", + "2026-02-06T20:00", + "2026-02-06T21:00", + "2026-02-06T22:00", + "2026-02-06T23:00", + "2026-02-07T00:00", + "2026-02-07T01:00", + "2026-02-07T02:00", + "2026-02-07T03:00", + "2026-02-07T04:00", + "2026-02-07T05:00", + "2026-02-07T06:00", + "2026-02-07T07:00", + "2026-02-07T08:00", + "2026-02-07T09:00", + "2026-02-07T10:00", + "2026-02-07T11:00", + "2026-02-07T12:00", + "2026-02-07T13:00", + "2026-02-07T14:00", + "2026-02-07T15:00", + "2026-02-07T16:00", + "2026-02-07T17:00", + "2026-02-07T18:00", + "2026-02-07T19:00", + "2026-02-07T20:00", + "2026-02-07T21:00", + "2026-02-07T22:00", + "2026-02-07T23:00", + "2026-02-08T00:00", + "2026-02-08T01:00", + "2026-02-08T02:00", + "2026-02-08T03:00", + "2026-02-08T04:00", + "2026-02-08T05:00", + "2026-02-08T06:00", + "2026-02-08T07:00", + "2026-02-08T08:00", + "2026-02-08T09:00", + "2026-02-08T10:00", + "2026-02-08T11:00", + "2026-02-08T12:00", + "2026-02-08T13:00", + "2026-02-08T14:00", + "2026-02-08T15:00", + "2026-02-08T16:00", + "2026-02-08T17:00", + "2026-02-08T18:00", + "2026-02-08T19:00", + "2026-02-08T20:00", + "2026-02-08T21:00", + "2026-02-08T22:00", + "2026-02-08T23:00", + "2026-02-09T00:00", + "2026-02-09T01:00", + "2026-02-09T02:00", + "2026-02-09T03:00", + "2026-02-09T04:00", + "2026-02-09T05:00", + "2026-02-09T06:00", + "2026-02-09T07:00", + "2026-02-09T08:00", + "2026-02-09T09:00", + "2026-02-09T10:00", + "2026-02-09T11:00", + "2026-02-09T12:00", + "2026-02-09T13:00", + "2026-02-09T14:00", + "2026-02-09T15:00", + "2026-02-09T16:00", + "2026-02-09T17:00", + "2026-02-09T18:00", + "2026-02-09T19:00", + "2026-02-09T20:00", + "2026-02-09T21:00", + "2026-02-09T22:00", + "2026-02-09T23:00" + ], + "temperature_2m": [ + 2.2, + 2.2, + 2.2, + 2.2, + 2.0, + 1.9, + 1.8, + 1.5, + 1.2, + 1.4, + 1.8, + 2.2, + 2.5, + 1.9, + 2.6, + 3.0, + 2.7, + 3.0, + 3.0, + 2.4, + 1.7, + 1.3, + 0.9, + -0.7, + -0.2, + -0.1, + -0.5, + -0.7, + -1.4, + -1.1, + -1.0, + -1.0, + -1.1, + -0.9, + 1.7, + 3.7, + 5.0, + 5.8, + 6.5, + 6.9, + 6.6, + 5.9, + 4.8, + 3.6, + 2.7, + 2.0, + 1.5, + 1.1, + 0.5, + 0.2, + 0.4, + 0.9, + 1.2, + 1.3, + 1.5, + 1.7, + 1.8, + 2.0, + 2.2, + 2.7, + 3.4, + 4.2, + 5.2, + 6.1, + 6.4, + 5.9, + 4.5, + 3.2, + 2.4, + 1.9, + 1.4, + 1.1, + 1.0, + 0.8, + 0.5, + 0.3, + 0.1, + -0.1, + -0.2, + -0.3, + -0.3, + -0.1, + 0.7, + 1.7, + 2.9, + 4.1, + 5.0, + 5.2, + 5.1, + 4.8, + 4.3, + 4.1, + 4.2, + 4.1, + 3.8, + 3.5, + 3.3, + 3.4, + 3.4, + 3.5, + 3.5, + 3.5, + 3.4, + 3.4, + 3.5, + 3.6, + 4.1, + 5.2, + 6.6, + 7.8, + 8.4, + 8.8, + 8.8, + 8.1, + 6.9, + 5.9, + 5.3, + 4.9, + 4.6, + 4.3, + 4.2, + 4.0, + 3.9, + 3.9, + 3.8, + 3.8, + 3.8, + 3.9, + 4.3, + 4.7, + 5.3, + 6.0, + 6.9, + 7.5, + 7.6, + 7.5, + 7.2, + 6.6, + 5.8, + 5.0, + 4.4, + 3.9, + 3.5, + 3.1, + 2.9, + 2.8, + 3.0, + 3.3, + 3.6, + 3.5, + 3.4, + 3.4, + 3.7, + 4.2, + 4.7, + 5.3, + 6.0, + 6.5, + 6.8, + 6.8, + 6.6, + 5.9, + 4.8, + 3.9, + 3.5, + 3.3, + 3.1, + 2.9 + ], + "rain": [ + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.10, + 0.40, + 0.00, + 0.00, + 0.20, + 0.60, + 0.30, + 1.30, + 0.00, + 0.00, + 0.40, + 0.10, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.10, + 0.20, + 0.00, + 0.00, + 0.00, + 0.10, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00, + 0.00 + ] + }, + "daily_units": { + "time": "iso8601", + "temperature_2m_max": "°C", + "temperature_2m_min": "°C", + "weather_code": "wmo code" + }, + "daily": { + "time": [ + "2026-02-03", + "2026-02-04", + "2026-02-05", + "2026-02-06", + "2026-02-07", + "2026-02-08", + "2026-02-09" + ], + "temperature_2m_max": [ + 3.0, + 6.9, + 6.4, + 5.2, + 8.8, + 7.6, + 6.8 + ], + "temperature_2m_min": [ + -0.7, + -1.4, + 0.2, + -0.3, + 3.3, + 3.1, + 2.8 + ], + "weather_code": [ + 61, + 45, + 45, + 61, + 61, + 3, + 3 + ] + } +} \ No newline at end of file