add forecast data, feels like fields (leaking test)
This commit is contained in:
parent
8df2877644
commit
7fd9810c78
9 changed files with 272 additions and 5 deletions
|
|
@ -79,8 +79,8 @@ curl "http://localhost:8002/London?lang=us&format=2&m"
|
|||
# Output: 51.5074,-0.1278: ☁️ 12°C 🌬️SW30km/h
|
||||
|
||||
# Test from US IP (automatic detection)
|
||||
curl -H "X-Forwarded-For: 8.8.8.8" "http://localhost:8002/London?format=2"
|
||||
# Output: Uses imperial if 8.8.8.8 is detected as US IP
|
||||
curl -H "X-Forwarded-For: 1.1.1.1" "http://localhost:8002/London?format=2"
|
||||
# Output: Uses imperial as 1.1.1.1 is detected as US IP
|
||||
```
|
||||
|
||||
## Documentation Updates
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ Router
|
|||
↓
|
||||
Request Handler
|
||||
↓
|
||||
Cache Check
|
||||
↓ (miss)
|
||||
Location Resolver
|
||||
↓
|
||||
Provider interface cache check
|
||||
↓ (miss)
|
||||
Weather Provider (interface)
|
||||
├─ MetNo (default)
|
||||
└─ Mock (tests)
|
||||
|
|
|
|||
|
|
@ -153,6 +153,8 @@ test "render with imperial units" {
|
|||
.location = "Chicago",
|
||||
.current = .{
|
||||
.temp_c = 10.0,
|
||||
.feels_like_c = 10.0,
|
||||
.feels_like_f = 10.0 * 1.8 + 32,
|
||||
.temp_f = 50.0,
|
||||
.condition = "Clear",
|
||||
.weather_code = .clear,
|
||||
|
|
@ -179,6 +181,8 @@ test "clear weather art" {
|
|||
.location = "Test",
|
||||
.current = .{
|
||||
.temp_c = 20.0,
|
||||
.feels_like_c = 20.0,
|
||||
.feels_like_f = 20.0 * 1.8 + 32,
|
||||
.temp_f = 68.0,
|
||||
.condition = "Clear",
|
||||
.weather_code = .clear,
|
||||
|
|
@ -204,6 +208,8 @@ test "partly cloudy weather art" {
|
|||
.location = "Test",
|
||||
.current = .{
|
||||
.temp_c = 18.0,
|
||||
.feels_like_c = 18.0,
|
||||
.feels_like_f = 18.0 * 1.8 + 32,
|
||||
.temp_f = 64.0,
|
||||
.condition = "Partly cloudy",
|
||||
.weather_code = .clouds_few,
|
||||
|
|
@ -229,6 +235,8 @@ test "cloudy weather art" {
|
|||
.location = "Test",
|
||||
.current = .{
|
||||
.temp_c = 15.0,
|
||||
.feels_like_c = 15.0,
|
||||
.feels_like_f = 15.0 * 1.8 + 32,
|
||||
.temp_f = 59.0,
|
||||
.condition = "Cloudy",
|
||||
.weather_code = .clouds_overcast,
|
||||
|
|
@ -254,6 +262,8 @@ test "rain weather art" {
|
|||
.location = "Test",
|
||||
.current = .{
|
||||
.temp_c = 12.0,
|
||||
.feels_like_c = 12.0,
|
||||
.feels_like_f = 12.0 * 1.8 + 32,
|
||||
.temp_f = 54.0,
|
||||
.condition = "Rain",
|
||||
.weather_code = .rain_moderate,
|
||||
|
|
@ -278,6 +288,8 @@ test "thunderstorm weather art" {
|
|||
.location = "Test",
|
||||
.current = .{
|
||||
.temp_c = 14.0,
|
||||
.feels_like_c = 14.0,
|
||||
.feels_like_f = 14.0 * 1.8 + 32,
|
||||
.temp_f = 57.0,
|
||||
.condition = "Thunderstorm",
|
||||
.weather_code = .thunderstorm,
|
||||
|
|
@ -302,6 +314,8 @@ test "snow weather art" {
|
|||
.location = "Test",
|
||||
.current = .{
|
||||
.temp_c = -2.0,
|
||||
.feels_like_c = -2.0,
|
||||
.feels_like_f = -2.0 * 1.8 + 32,
|
||||
.temp_f = 28.0,
|
||||
.condition = "Snow",
|
||||
.weather_code = .snow,
|
||||
|
|
@ -326,6 +340,8 @@ test "sleet weather art" {
|
|||
.location = "Test",
|
||||
.current = .{
|
||||
.temp_c = 0.0,
|
||||
.feels_like_c = 0.0,
|
||||
.feels_like_f = 0.0 * 1.8 + 32,
|
||||
.temp_f = 32.0,
|
||||
.condition = "Sleet",
|
||||
.weather_code = .sleet,
|
||||
|
|
@ -350,6 +366,8 @@ test "fog weather art" {
|
|||
.location = "Test",
|
||||
.current = .{
|
||||
.temp_c = 8.0,
|
||||
.feels_like_c = 8.0,
|
||||
.feels_like_f = 8.0 * 1.8 + 32,
|
||||
.temp_f = 46.0,
|
||||
.condition = "Fog",
|
||||
.weather_code = .fog,
|
||||
|
|
@ -374,6 +392,8 @@ test "unknown weather code art" {
|
|||
.location = "Test",
|
||||
.current = .{
|
||||
.temp_c = 16.0,
|
||||
.feels_like_c = 16.0,
|
||||
.feels_like_f = 16.0 * 1.8 + 32,
|
||||
.temp_f = 61.0,
|
||||
.condition = "Unknown",
|
||||
.weather_code = .unknown,
|
||||
|
|
@ -401,6 +421,8 @@ test "temperature matches between ansi and custom format" {
|
|||
.location = "PDX",
|
||||
.current = .{
|
||||
.temp_c = 13.1,
|
||||
.feels_like_c = 13.1,
|
||||
.feels_like_f = 13.1 * 1.8 + 32,
|
||||
.temp_f = 55.6,
|
||||
.condition = "Clear",
|
||||
.weather_code = .clear,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ test "render custom format with location and temp" {
|
|||
.location = "London",
|
||||
.current = .{
|
||||
.temp_c = 7.0,
|
||||
.feels_like_c = 7.0,
|
||||
.feels_like_f = 7.0 * 1.8 + 32,
|
||||
.temp_f = 44.6,
|
||||
.condition = "Overcast",
|
||||
.weather_code = .clouds_overcast,
|
||||
|
|
@ -109,6 +111,8 @@ test "render custom format with newline" {
|
|||
.location = "Paris",
|
||||
.current = .{
|
||||
.temp_c = 10.0,
|
||||
.feels_like_c = 10.0,
|
||||
.feels_like_f = 10.0 * 1.8 + 32,
|
||||
.temp_f = 50.0,
|
||||
.condition = "Clear",
|
||||
.weather_code = .clear,
|
||||
|
|
@ -135,6 +139,8 @@ test "render custom format with humidity and pressure" {
|
|||
.location = "Berlin",
|
||||
.current = .{
|
||||
.temp_c = 5.0,
|
||||
.feels_like_c = 5.0,
|
||||
.feels_like_f = 5.0 * 1.8 + 32,
|
||||
.temp_f = 41.0,
|
||||
.condition = "Cloudy",
|
||||
.weather_code = .clouds_overcast,
|
||||
|
|
@ -162,6 +168,8 @@ test "render custom format with imperial units" {
|
|||
.location = "NYC",
|
||||
.current = .{
|
||||
.temp_c = 10.0,
|
||||
.feels_like_c = 10.0,
|
||||
.feels_like_f = 10.0 * 1.8 + 32,
|
||||
.temp_f = 50.0,
|
||||
.condition = "Clear",
|
||||
.weather_code = .clear,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ test "render json format" {
|
|||
.location = "London",
|
||||
.current = .{
|
||||
.temp_c = 15.0,
|
||||
.feels_like_c = 15.0,
|
||||
.feels_like_f = 15.0 * 1.8 + 32,
|
||||
.temp_f = 59.0,
|
||||
.condition = "Partly cloudy",
|
||||
.weather_code = .clouds_few,
|
||||
|
|
|
|||
|
|
@ -122,6 +122,8 @@ test "format 1" {
|
|||
.location = "London",
|
||||
.current = .{
|
||||
.temp_c = 15.0,
|
||||
.feels_like_c = 15.0,
|
||||
.feels_like_f = 15.0 * 1.8 + 32,
|
||||
.temp_f = 59.0,
|
||||
.condition = "Clear",
|
||||
.weather_code = .clear,
|
||||
|
|
@ -146,6 +148,8 @@ test "custom format" {
|
|||
.location = "London",
|
||||
.current = .{
|
||||
.temp_c = 15.0,
|
||||
.feels_like_c = 15.0,
|
||||
.feels_like_f = 15.0 * 1.8 + 32,
|
||||
.temp_f = 59.0,
|
||||
.condition = "Clear",
|
||||
.weather_code = .clear,
|
||||
|
|
@ -170,6 +174,8 @@ test "format 2 with imperial units" {
|
|||
.location = "Portland",
|
||||
.current = .{
|
||||
.temp_c = 10.0,
|
||||
.feels_like_c = 10.0,
|
||||
.feels_like_f = 10.0 * 1.8 + 32,
|
||||
.temp_f = 50.0,
|
||||
.condition = "Cloudy",
|
||||
.weather_code = .clouds_overcast,
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ test "render v2 format" {
|
|||
.location = "Munich",
|
||||
.current = .{
|
||||
.temp_c = 12.0,
|
||||
.feels_like_c = 12.0,
|
||||
.feels_like_f = 12.0 * 1.8 + 32,
|
||||
.temp_f = 53.6,
|
||||
.condition = "Overcast",
|
||||
.weather_code = .clouds_overcast,
|
||||
|
|
@ -103,6 +105,8 @@ test "render v2 format with imperial units" {
|
|||
.location = "Boston",
|
||||
.current = .{
|
||||
.temp_c = 10.0,
|
||||
.feels_like_c = 10.0,
|
||||
.feels_like_f = 10.0 * 1.8 + 32,
|
||||
.temp_f = 50.0,
|
||||
.condition = "Clear",
|
||||
.weather_code = .clear,
|
||||
|
|
|
|||
|
|
@ -178,11 +178,18 @@ fn parseMetNoResponse(allocator: std.mem.Allocator, coords: Coordinates, json: s
|
|||
else
|
||||
"N";
|
||||
|
||||
// Parse forecast days from timeseries
|
||||
const forecast = try parseForecastDays(allocator, timeseries.array.items);
|
||||
|
||||
const feels_like_c = temp_c; // TODO: Calculate wind chill
|
||||
|
||||
return types.WeatherData{
|
||||
.location = try std.fmt.allocPrint(allocator, "{d:.4},{d:.4}", .{ coords.latitude, coords.longitude }),
|
||||
.current = .{
|
||||
.temp_c = temp_c,
|
||||
.temp_f = temp_c * 9.0 / 5.0 + 32.0,
|
||||
.feels_like_c = feels_like_c,
|
||||
.feels_like_f = feels_like_c * 9.0 / 5.0 + 32.0,
|
||||
.condition = try allocator.dupe(u8, symbolCodeToCondition(symbol_code)),
|
||||
.weather_code = symbolCodeToWeatherCode(symbol_code),
|
||||
.humidity = humidity,
|
||||
|
|
@ -191,11 +198,174 @@ fn parseMetNoResponse(allocator: std.mem.Allocator, coords: Coordinates, json: s
|
|||
.pressure_mb = pressure_mb,
|
||||
.precip_mm = 0.0,
|
||||
},
|
||||
.forecast = &.{},
|
||||
.forecast = forecast,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
fn clearHourlyForecast(allocator: std.mem.Allocator, forecast: *std.ArrayList(types.HourlyForecast)) void {
|
||||
for (forecast.items) |h| {
|
||||
allocator.free(h.time);
|
||||
allocator.free(h.condition);
|
||||
}
|
||||
forecast.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
fn parseForecastDays(allocator: std.mem.Allocator, timeseries: []std.json.Value) ![]types.ForecastDay {
|
||||
var days: std.ArrayList(types.ForecastDay) = .empty;
|
||||
errdefer days.deinit(allocator);
|
||||
|
||||
var current_date: ?[]const u8 = null;
|
||||
var day_temps: std.ArrayList(f32) = .empty;
|
||||
defer day_temps.deinit(allocator);
|
||||
var day_hourly: std.ArrayList(types.HourlyForecast) = .empty;
|
||||
var day_all_hours: std.ArrayList(types.HourlyForecast) = .empty;
|
||||
defer day_all_hours.deinit(allocator);
|
||||
var day_symbol: ?[]const u8 = null;
|
||||
|
||||
for (timeseries) |entry| {
|
||||
const time_str = entry.object.get("time").?.string;
|
||||
const date = time_str[0..10];
|
||||
const hour = time_str[11..13];
|
||||
|
||||
const data = entry.object.get("data") orelse continue;
|
||||
const instant = data.object.get("instant") orelse continue;
|
||||
const details_obj = instant.object.get("details") orelse continue;
|
||||
const temp: f32 = @floatCast(details_obj.object.get("air_temperature").?.float);
|
||||
const wind_ms = details_obj.object.get("wind_speed") orelse continue;
|
||||
const wind_kph: f32 = @floatCast(wind_ms.float * 3.6);
|
||||
|
||||
if (current_date == null or !std.mem.eql(u8, current_date.?, date)) {
|
||||
// Save previous day if exists
|
||||
if (current_date != null and day_temps.items.len > 0) {
|
||||
var max_temp: f32 = day_temps.items[0];
|
||||
var min_temp: f32 = day_temps.items[0];
|
||||
for (day_temps.items) |t| {
|
||||
if (t > max_temp) max_temp = t;
|
||||
if (t < min_temp) min_temp = t;
|
||||
}
|
||||
|
||||
const symbol = day_symbol orelse "clearsky_day";
|
||||
|
||||
// Use preferred times if we have 4, otherwise pick 4 evenly spaced from all hours
|
||||
const hourly_slice: []types.HourlyForecast = blk: {
|
||||
if (day_hourly.items.len >= 4) {
|
||||
const hrs = try day_hourly.toOwnedSlice(allocator);
|
||||
clearHourlyForecast(allocator, &day_all_hours);
|
||||
break :blk hrs;
|
||||
}
|
||||
|
||||
clearHourlyForecast(allocator, &day_all_hours);
|
||||
|
||||
if (day_all_hours.items.len < 4)
|
||||
break :blk try day_all_hours.toOwnedSlice(allocator);
|
||||
// Pick 4 evenly spaced entries from day_all_hours
|
||||
if (day_all_hours.items.len >= 4) {
|
||||
const step = day_all_hours.items.len / 4;
|
||||
var selected: std.ArrayList(types.HourlyForecast) = .empty;
|
||||
try selected.append(allocator, day_all_hours.items[0]);
|
||||
try selected.append(allocator, day_all_hours.items[step]);
|
||||
try selected.append(allocator, day_all_hours.items[step * 2]);
|
||||
try selected.append(allocator, day_all_hours.items[step * 3]);
|
||||
const hrs = try selected.toOwnedSlice(allocator);
|
||||
|
||||
// Free the rest
|
||||
for (day_all_hours.items, 0..) |h, i| {
|
||||
if (i != 0 and i != step and i != step * 2 and i != step * 3) {
|
||||
allocator.free(h.time);
|
||||
allocator.free(h.condition);
|
||||
}
|
||||
}
|
||||
day_all_hours.clearRetainingCapacity();
|
||||
break :blk hrs;
|
||||
}
|
||||
break :blk try day_all_hours.toOwnedSlice(allocator);
|
||||
};
|
||||
|
||||
try days.append(allocator, .{
|
||||
.date = try allocator.dupe(u8, current_date.?),
|
||||
.max_temp_c = max_temp,
|
||||
.min_temp_c = min_temp,
|
||||
.condition = try allocator.dupe(u8, symbolCodeToCondition(symbol)),
|
||||
.weather_code = symbolCodeToWeatherCode(symbol),
|
||||
.hourly = hourly_slice,
|
||||
});
|
||||
|
||||
if (days.items.len >= 3) break;
|
||||
}
|
||||
|
||||
// Start new day
|
||||
current_date = date;
|
||||
day_temps.clearRetainingCapacity();
|
||||
day_all_hours.clearRetainingCapacity();
|
||||
day_symbol = null;
|
||||
}
|
||||
|
||||
try day_temps.append(allocator, temp);
|
||||
|
||||
// Collect ALL hourly forecasts
|
||||
const next_1h = data.object.get("next_1_hours");
|
||||
if (next_1h) |n1h| {
|
||||
const symbol_code = n1h.object.get("summary").?.object.get("symbol_code").?.string;
|
||||
const precip = if (n1h.object.get("details")) |det| blk: {
|
||||
if (det.object.get("precipitation_amount")) |p| {
|
||||
break :blk @as(f32, @floatCast(p.float));
|
||||
}
|
||||
break :blk @as(f32, 0.0);
|
||||
} else 0.0;
|
||||
|
||||
try day_all_hours.append(allocator, .{
|
||||
.time = try allocator.dupe(u8, time_str[11..16]),
|
||||
.temp_c = temp,
|
||||
.feels_like_c = temp,
|
||||
.condition = try allocator.dupe(u8, symbolCodeToCondition(symbol_code)),
|
||||
.weather_code = symbolCodeToWeatherCode(symbol_code),
|
||||
.wind_kph = wind_kph,
|
||||
.precip_mm = precip,
|
||||
});
|
||||
}
|
||||
|
||||
// Collect preferred hourly forecasts for 06:00, 12:00, 18:00, 00:00
|
||||
if (std.mem.eql(u8, hour, "06") or std.mem.eql(u8, hour, "12") or
|
||||
std.mem.eql(u8, hour, "18") or std.mem.eql(u8, hour, "00")) {
|
||||
if (next_1h) |n1h| {
|
||||
const symbol_code = n1h.object.get("summary").?.object.get("symbol_code").?.string;
|
||||
const precip = if (n1h.object.get("details")) |det| blk: {
|
||||
if (det.object.get("precipitation_amount")) |p| {
|
||||
break :blk @as(f32, @floatCast(p.float));
|
||||
}
|
||||
break :blk @as(f32, 0.0);
|
||||
} else 0.0;
|
||||
|
||||
try day_hourly.append(allocator, .{
|
||||
.time = try allocator.dupe(u8, time_str[11..16]),
|
||||
.temp_c = temp,
|
||||
.feels_like_c = temp,
|
||||
.condition = try allocator.dupe(u8, symbolCodeToCondition(symbol_code)),
|
||||
.weather_code = symbolCodeToWeatherCode(symbol_code),
|
||||
.wind_kph = wind_kph,
|
||||
.precip_mm = precip,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get symbol for the day
|
||||
if (day_symbol == null) {
|
||||
const next_6h = data.object.get("next_6_hours");
|
||||
if (next_6h) |n6h| {
|
||||
if (n6h.object.get("summary")) |summary| {
|
||||
day_symbol = summary.object.get("symbol_code").?.string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget to deinit day_hourly if we didn't transfer ownership
|
||||
day_hourly.deinit(allocator);
|
||||
|
||||
return days.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn symbolCodeToWeatherCode(symbol: []const u8) types.WeatherCode {
|
||||
var it = std.mem.splitScalar(u8, symbol, '_');
|
||||
const metno_weather_code = it.next().?;
|
||||
|
|
@ -244,3 +414,55 @@ test "symbolCodeToWeatherCode" {
|
|||
try std.testing.expectEqual(types.WeatherCode.rain_light, symbolCodeToWeatherCode("lightrain"));
|
||||
try std.testing.expectEqual(types.WeatherCode.snow, symbolCodeToWeatherCode("snow"));
|
||||
}
|
||||
|
||||
test "parseForecastDays extracts 3 days" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const json_str =
|
||||
\\{"properties":{"timeseries":[
|
||||
\\ {"time":"2025-12-20T00:00:00Z","data":{"instant":{"details":{"air_temperature":10.0,"wind_speed":5.0}},"next_6_hours":{"summary":{"symbol_code":"clearsky_day"}}}},
|
||||
\\ {"time":"2025-12-20T06:00:00Z","data":{"instant":{"details":{"air_temperature":15.0,"wind_speed":6.0}},"next_1_hours":{"summary":{"symbol_code":"clearsky_day"}}}},
|
||||
\\ {"time":"2025-12-20T12:00:00Z","data":{"instant":{"details":{"air_temperature":20.0,"wind_speed":7.0}},"next_1_hours":{"summary":{"symbol_code":"clearsky_day"}}}},
|
||||
\\ {"time":"2025-12-20T18:00:00Z","data":{"instant":{"details":{"air_temperature":12.0,"wind_speed":5.0}},"next_1_hours":{"summary":{"symbol_code":"clearsky_day"}}}},
|
||||
\\ {"time":"2025-12-21T00:00:00Z","data":{"instant":{"details":{"air_temperature":8.0,"wind_speed":8.0}},"next_6_hours":{"summary":{"symbol_code":"rain"}},"next_1_hours":{"summary":{"symbol_code":"rain"}}}},
|
||||
\\ {"time":"2025-12-21T12:00:00Z","data":{"instant":{"details":{"air_temperature":14.0,"wind_speed":10.0}},"next_1_hours":{"summary":{"symbol_code":"rain"}}}},
|
||||
\\ {"time":"2025-12-22T00:00:00Z","data":{"instant":{"details":{"air_temperature":5.0,"wind_speed":4.0}},"next_6_hours":{"summary":{"symbol_code":"snow"}},"next_1_hours":{"summary":{"symbol_code":"snow"}}}},
|
||||
\\ {"time":"2025-12-22T12:00:00Z","data":{"instant":{"details":{"air_temperature":7.0,"wind_speed":3.0}},"next_1_hours":{"summary":{"symbol_code":"snow"}}}},
|
||||
\\ {"time":"2025-12-23T00:00:00Z","data":{"instant":{"details":{"air_temperature":6.0,"wind_speed":2.0}}}}
|
||||
\\]}}
|
||||
;
|
||||
|
||||
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_str, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const properties = parsed.value.object.get("properties").?;
|
||||
const timeseries = properties.object.get("timeseries").?;
|
||||
|
||||
const forecast = try parseForecastDays(allocator, timeseries.array.items);
|
||||
defer {
|
||||
for (forecast) |day| {
|
||||
allocator.free(day.date);
|
||||
allocator.free(day.condition);
|
||||
for (day.hourly) |hour| {
|
||||
allocator.free(hour.time);
|
||||
allocator.free(hour.condition);
|
||||
}
|
||||
allocator.free(day.hourly);
|
||||
}
|
||||
allocator.free(forecast);
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 3), forecast.len);
|
||||
try std.testing.expectEqualStrings("2025-12-20", forecast[0].date);
|
||||
try std.testing.expectEqual(@as(f32, 20.0), forecast[0].max_temp_c);
|
||||
try std.testing.expectEqual(@as(f32, 10.0), forecast[0].min_temp_c);
|
||||
try std.testing.expectEqual(types.WeatherCode.clear, forecast[0].weather_code);
|
||||
}
|
||||
|
||||
test "parseForecastDays handles empty timeseries" {
|
||||
const allocator = std.testing.allocator;
|
||||
const empty: []std.json.Value = &.{};
|
||||
const forecast = try parseForecastDays(allocator, empty);
|
||||
defer allocator.free(forecast);
|
||||
try std.testing.expectEqual(@as(usize, 0), forecast.len);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ pub const WeatherData = struct {
|
|||
pub const CurrentCondition = struct {
|
||||
temp_c: f32,
|
||||
temp_f: f32,
|
||||
feels_like_c: f32,
|
||||
feels_like_f: f32,
|
||||
condition: []const u8,
|
||||
weather_code: WeatherCode,
|
||||
humidity: u8,
|
||||
|
|
@ -126,6 +128,7 @@ pub const ForecastDay = struct {
|
|||
pub const HourlyForecast = struct {
|
||||
time: []const u8,
|
||||
temp_c: f32,
|
||||
feels_like_c: f32,
|
||||
condition: []const u8,
|
||||
weather_code: WeatherCode,
|
||||
wind_kph: f32,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue