wttr/src/weather/MetNo.zig

214 lines
7.9 KiB
Zig

const std = @import("std");
const weather_provider = @import("provider.zig");
const types = @import("types.zig");
const MetNo = @This();
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) !MetNo {
return MetNo{
.allocator = allocator,
};
}
pub fn provider(self: *MetNo) weather_provider.WeatherProvider {
return .{
.ptr = self,
.vtable = &.{
.fetch = fetch,
.deinit = deinitProvider,
},
};
}
fn fetch(ptr: *anyopaque, allocator: std.mem.Allocator, location: []const u8) !types.WeatherData {
const self: *MetNo = @ptrCast(@alignCast(ptr));
// Parse location as "lat,lon" or use default
const coords = parseLocation(location) catch Coords{ .lat = 51.5074, .lon = -0.1278 };
const url = try std.fmt.allocPrint(
self.allocator,
"https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={d:.4}&lon={d:.4}",
.{ coords.lat, coords.lon },
);
defer self.allocator.free(url);
// Fetch weather data from met.no API
var client = std.http.Client{ .allocator = self.allocator };
defer client.deinit();
const uri = try std.Uri.parse(url);
var response_buf: [1024 * 1024]u8 = undefined;
var writer = std.Io.Writer.fixed(&response_buf);
const result = try client.fetch(.{
.location = .{ .uri = uri },
.method = .GET,
.response_writer = &writer,
.extra_headers = &.{
.{ .name = "User-Agent", .value = "wttr.in-zig/1.0 github.com/chubin/wttr.in" },
},
});
if (result.status != .ok) {
return error.WeatherApiFailed;
}
const response_body = response_buf[0..writer.end];
// Parse JSON response
const parsed = try std.json.parseFromSlice(
std.json.Value,
allocator,
response_body,
.{},
);
defer parsed.deinit();
return try parseMetNoResponse(allocator, location, parsed.value);
}
fn deinitProvider(ptr: *anyopaque) void {
const self: *MetNo = @ptrCast(@alignCast(ptr));
self.deinit();
}
pub fn deinit(self: *MetNo) void {
_ = self;
}
const Coords = struct {
lat: f64,
lon: f64,
};
fn parseLocation(location: []const u8) !Coords {
if (std.mem.indexOf(u8, location, ",")) |comma_idx| {
const lat_str = std.mem.trim(u8, location[0..comma_idx], " ");
const lon_str = std.mem.trim(u8, location[comma_idx + 1 ..], " ");
return Coords{
.lat = try std.fmt.parseFloat(f64, lat_str),
.lon = try std.fmt.parseFloat(f64, lon_str),
};
}
return error.InvalidLocationFormat;
}
fn parseMetNoResponse(allocator: std.mem.Allocator, location: []const u8, json: std.json.Value) !types.WeatherData {
const properties = json.object.get("properties") orelse return error.InvalidResponse;
const timeseries = properties.object.get("timeseries") orelse return error.InvalidResponse;
if (timeseries.array.items.len == 0) return error.InvalidResponse;
const current = timeseries.array.items[0];
const data = current.object.get("data") orelse return error.InvalidResponse;
const instant = data.object.get("instant") orelse return error.InvalidResponse;
const details = instant.object.get("details") orelse return error.InvalidResponse;
const temp_c = @as(f32, @floatCast(details.object.get("air_temperature").?.float));
const humidity = @as(u8, @intFromFloat(details.object.get("relative_humidity").?.float));
const wind_speed_ms = details.object.get("wind_speed").?.float;
const wind_kph = @as(f32, @floatCast(wind_speed_ms * 3.6));
const pressure_mb = @as(f32, @floatCast(details.object.get("air_pressure_at_sea_level").?.float));
// Get weather symbol
const next_1h = data.object.get("next_1_hours");
const symbol_code = if (next_1h) |n1h|
n1h.object.get("summary").?.object.get("symbol_code").?.string
else
"clearsky_day";
// Get wind direction
const wind_from_deg = details.object.get("wind_from_direction");
const wind_dir = if (wind_from_deg) |deg|
degreeToDirection(@as(f32, @floatCast(deg.float)))
else
"N";
return types.WeatherData{
.location = try allocator.dupe(u8, location),
.current = .{
.temp_c = temp_c,
.temp_f = temp_c * 9.0 / 5.0 + 32.0,
.condition = try allocator.dupe(u8, symbolCodeToCondition(symbol_code)),
.weather_code = symbolCodeToWeatherCode(symbol_code),
.humidity = humidity,
.wind_kph = wind_kph,
.wind_dir = try allocator.dupe(u8, wind_dir),
.pressure_mb = pressure_mb,
.precip_mm = 0.0,
},
.forecast = &.{},
.allocator = allocator,
};
}
fn symbolCodeToWeatherCode(symbol: []const u8) u16 {
if (std.mem.indexOf(u8, symbol, "clearsky")) |_| return 113;
if (std.mem.indexOf(u8, symbol, "fair")) |_| return 116;
if (std.mem.indexOf(u8, symbol, "partlycloudy")) |_| return 116;
if (std.mem.indexOf(u8, symbol, "cloudy")) |_| return 119;
if (std.mem.indexOf(u8, symbol, "fog")) |_| return 143;
if (std.mem.indexOf(u8, symbol, "rain")) |_| return 296;
if (std.mem.indexOf(u8, symbol, "sleet")) |_| return 362;
if (std.mem.indexOf(u8, symbol, "snow")) |_| return 338;
if (std.mem.indexOf(u8, symbol, "thunder")) |_| return 200;
return 113;
}
fn symbolCodeToCondition(symbol: []const u8) []const u8 {
if (std.mem.indexOf(u8, symbol, "clearsky")) |_| return "Clear";
if (std.mem.indexOf(u8, symbol, "fair")) |_| return "Fair";
if (std.mem.indexOf(u8, symbol, "partlycloudy")) |_| return "Partly cloudy";
if (std.mem.indexOf(u8, symbol, "cloudy")) |_| return "Cloudy";
if (std.mem.indexOf(u8, symbol, "fog")) |_| return "Fog";
if (std.mem.indexOf(u8, symbol, "rain")) |_| return "Rain";
if (std.mem.indexOf(u8, symbol, "sleet")) |_| return "Sleet";
if (std.mem.indexOf(u8, symbol, "snow")) |_| return "Snow";
if (std.mem.indexOf(u8, symbol, "thunder")) |_| return "Thunderstorm";
return "Clear";
}
fn degreeToDirection(deg: f32) []const u8 {
const normalized = @mod(deg + 22.5, 360.0);
const idx = @as(usize, @intFromFloat(normalized / 45.0));
const directions = [_][]const u8{ "N", "NE", "E", "SE", "S", "SW", "W", "NW" };
return directions[@min(idx, 7)];
}
test "parseLocation with valid coordinates" {
const coords = try parseLocation("51.5074,-0.1278");
try std.testing.expectApproxEqAbs(@as(f64, 51.5074), coords.lat, 0.0001);
try std.testing.expectApproxEqAbs(@as(f64, -0.1278), coords.lon, 0.0001);
}
test "parseLocation with whitespace" {
const coords = try parseLocation(" 40.7128 , -74.0060 ");
try std.testing.expectApproxEqAbs(@as(f64, 40.7128), coords.lat, 0.0001);
try std.testing.expectApproxEqAbs(@as(f64, -74.0060), coords.lon, 0.0001);
}
test "parseLocation with invalid format" {
try std.testing.expectError(error.InvalidLocationFormat, parseLocation("London"));
}
test "degreeToDirection" {
try std.testing.expectEqualStrings("N", degreeToDirection(0));
try std.testing.expectEqualStrings("NE", degreeToDirection(45));
try std.testing.expectEqualStrings("E", degreeToDirection(90));
try std.testing.expectEqualStrings("SE", degreeToDirection(135));
try std.testing.expectEqualStrings("S", degreeToDirection(180));
try std.testing.expectEqualStrings("SW", degreeToDirection(225));
try std.testing.expectEqualStrings("W", degreeToDirection(270));
try std.testing.expectEqualStrings("NW", degreeToDirection(315));
}
test "symbolCodeToWeatherCode" {
try std.testing.expectEqual(@as(u16, 113), symbolCodeToWeatherCode("clearsky_day"));
try std.testing.expectEqual(@as(u16, 116), symbolCodeToWeatherCode("fair_night"));
try std.testing.expectEqual(@as(u16, 119), symbolCodeToWeatherCode("cloudy"));
try std.testing.expectEqual(@as(u16, 296), symbolCodeToWeatherCode("lightrain"));
try std.testing.expectEqual(@as(u16, 338), symbolCodeToWeatherCode("snow"));
}