From 59198284fd058ef34d6e10596ed1bf9bf335f74b Mon Sep 17 00:00:00 2001 From: Stefan Koelle Date: Sun, 2 Aug 2026 10:28:56 +0200 Subject: [PATCH] Phase 0/2: API layer kept 1:1, only secrets.h include path fixed http_client/weather_api/calendar_api/departures_api are hardware- independent (HTTP GET + ArduinoJson parsing, same endpoints, same JSON). Kept verbatim per PLAN.md migration strategy; only changed #include "../../include/secrets.h" -> #include to match the PlatformIO include/ layout (no functional change). --- src/api/calendar_api.cpp | 27 +++++++++++++++++++++++ src/api/calendar_api.h | 19 ++++++++++++++++ src/api/departures_api.cpp | 30 ++++++++++++++++++++++++++ src/api/departures_api.h | 22 +++++++++++++++++++ src/api/http_client.cpp | 25 ++++++++++++++++++++++ src/api/http_client.h | 10 +++++++++ src/api/weather_api.cpp | 44 ++++++++++++++++++++++++++++++++++++++ src/api/weather_api.h | 27 +++++++++++++++++++++++ 8 files changed, 204 insertions(+) create mode 100644 src/api/calendar_api.cpp create mode 100644 src/api/calendar_api.h create mode 100644 src/api/departures_api.cpp create mode 100644 src/api/departures_api.h create mode 100644 src/api/http_client.cpp create mode 100644 src/api/http_client.h create mode 100644 src/api/weather_api.cpp create mode 100644 src/api/weather_api.h diff --git a/src/api/calendar_api.cpp b/src/api/calendar_api.cpp new file mode 100644 index 0000000..27aabec --- /dev/null +++ b/src/api/calendar_api.cpp @@ -0,0 +1,27 @@ +#include "calendar_api.h" +#include "http_client.h" +#include +#include + +CalendarData fetchCalendar() { + CalendarData data; + ApiResult res = httpGet(CALENDAR_API_URL); + if (!res.success) { data.valid = false; return data; } + + DynamicJsonDocument doc(8192); + if (deserializeJson(doc, res.body)) { data.valid = false; return data; } + + JsonArray events = doc["events"]; + for (JsonObject e : events) { + CalendarEvent ev; + ev.id = e["id"] | 0; + ev.summary = e["summary"] | ""; + ev.startAt = e["start_at"] | ""; + ev.endAt = e["end_at"] | ""; + ev.allDay = e["all_day"] | false; + ev.status = e["status"] | ""; + data.events.push_back(ev); + } + data.valid = true; + return data; +} diff --git a/src/api/calendar_api.h b/src/api/calendar_api.h new file mode 100644 index 0000000..f28be54 --- /dev/null +++ b/src/api/calendar_api.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +struct CalendarEvent { + long id = 0; + String summary; + String startAt; + String endAt; + bool allDay = false; + String status; +}; + +struct CalendarData { + bool valid = false; + std::vector events; +}; + +CalendarData fetchCalendar(); diff --git a/src/api/departures_api.cpp b/src/api/departures_api.cpp new file mode 100644 index 0000000..0688faa --- /dev/null +++ b/src/api/departures_api.cpp @@ -0,0 +1,30 @@ +#include "departures_api.h" +#include "http_client.h" +#include +#include + +DeparturesData fetchDepartures() { + DeparturesData data; + ApiResult res = httpGet(DEPARTURES_API_URL); + if (!res.success) { data.valid = false; return data; } + + DynamicJsonDocument doc(16384); + if (deserializeJson(doc, res.body)) { data.valid = false; return data; } + + JsonArray departures = doc["departures"]; + for (JsonObject d : departures) { + Departure dep; + dep.station = d["station"] | ""; + dep.type = d["type"] | ""; + dep.icon = d["icon"] | ""; + dep.line = d["line"] | ""; + dep.destination = d["destination"] | ""; + dep.timeEpoch = d["time_epoch"] | 0; + dep.timeStr = d["time_str"] | ""; + dep.delayMin = d["delay_min"] | 0; + dep.cancelled = d["cancelled"] | false; + data.departures.push_back(dep); + } + data.valid = true; + return data; +} diff --git a/src/api/departures_api.h b/src/api/departures_api.h new file mode 100644 index 0000000..3ebef16 --- /dev/null +++ b/src/api/departures_api.h @@ -0,0 +1,22 @@ +#pragma once +#include +#include + +struct Departure { + String station; + String type; + String icon; + String line; + String destination; + long timeEpoch = 0; + String timeStr; + int delayMin = 0; + bool cancelled = false; +}; + +struct DeparturesData { + bool valid = false; + std::vector departures; +}; + +DeparturesData fetchDepartures(); diff --git a/src/api/http_client.cpp b/src/api/http_client.cpp new file mode 100644 index 0000000..5c93e7a --- /dev/null +++ b/src/api/http_client.cpp @@ -0,0 +1,25 @@ +#include "http_client.h" +#include +#include + +ApiResult httpGet(const String &url, uint32_t timeoutMs) { + ApiResult result; + if (WiFi.status() != WL_CONNECTED) { + result.success = false; + result.httpCode = -1; + return result; + } + HTTPClient http; + http.setTimeout(timeoutMs); + http.begin(url); + int code = http.GET(); + result.httpCode = code; + if (code == HTTP_CODE_OK) { + result.body = http.getString(); + result.success = true; + } else { + result.success = false; + } + http.end(); + return result; +} diff --git a/src/api/http_client.h b/src/api/http_client.h new file mode 100644 index 0000000..5c8c6eb --- /dev/null +++ b/src/api/http_client.h @@ -0,0 +1,10 @@ +#pragma once +#include + +struct ApiResult { + bool success = false; + String body; + int httpCode = -1; +}; + +ApiResult httpGet(const String &url, uint32_t timeoutMs = 5000); diff --git a/src/api/weather_api.cpp b/src/api/weather_api.cpp new file mode 100644 index 0000000..20e147c --- /dev/null +++ b/src/api/weather_api.cpp @@ -0,0 +1,44 @@ +#include "weather_api.h" +#include "http_client.h" +#include +#include + +WeatherData fetchWeather() { + WeatherData data; + ApiResult res = httpGet(WEATHER_API_URL); + if (!res.success) { data.valid = false; return data; } + + DynamicJsonDocument doc(8192); + if (deserializeJson(doc, res.body)) { data.valid = false; return data; } + + JsonObject current = doc["current"]; + data.current.temperature = current["temperature"] | 0; + data.current.symbol = current["symbol"] | ""; + data.current.description = current["description"] | ""; + + JsonArray forecast = doc["forecast"]; + for (JsonObject entry : forecast) { + ForecastEntry fe; + fe.time = entry["time"] | ""; + fe.temperature = entry["temperature"] | 0; + fe.symbol = entry["symbol"] | ""; + fe.description = entry["description"] | ""; + JsonObject precip = entry["precipitation"]; + fe.precipitationProbability = precip["probability"] | 0.0f; + fe.precipitationType = precip["type"] | ""; + data.forecast.push_back(fe); + } + data.valid = true; + return data; +} + +bool willRainSoon(const WeatherData &data, int hours) { + if (!data.valid) return false; + int checked = 0; + for (const auto &entry : data.forecast) { + if (checked >= hours) break; + if (entry.precipitationType == "rain" && entry.precipitationProbability > 0.0f) return true; + checked++; + } + return false; +} diff --git a/src/api/weather_api.h b/src/api/weather_api.h new file mode 100644 index 0000000..73cf32e --- /dev/null +++ b/src/api/weather_api.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include + +struct WeatherCurrent { + int temperature = 0; + String symbol; + String description; +}; + +struct ForecastEntry { + String time; + int temperature = 0; + String symbol; + String description; + float precipitationProbability = 0.0f; + String precipitationType; +}; + +struct WeatherData { + bool valid = false; + WeatherCurrent current; + std::vector forecast; +}; + +WeatherData fetchWeather(); +bool willRainSoon(const WeatherData &data, int hours = 8);