AI: real met.no implementation

This commit is contained in:
Emil Lerch 2025-12-18 10:09:54 -08:00
parent 75e5e88c31
commit 20ed4151ff
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 83 additions and 14 deletions

View file

@ -111,7 +111,10 @@ fn handleWeatherInternal(
};
// Fetch weather using coordinates
const weather = opts.provider.fetch(allocator, location.name) catch |err| {
const coord_str = try std.fmt.allocPrint(allocator, "{d:.4},{d:.4}", .{ location.latitude, location.longitude });
defer allocator.free(coord_str);
const weather = opts.provider.fetch(allocator, coord_str) catch |err| {
switch (err) {
error.LocationNotFound => {
res.status = 404;

View file

@ -24,7 +24,8 @@ pub const MetNo = struct {
fn fetch(ptr: *anyopaque, allocator: std.mem.Allocator, location: []const u8) !types.WeatherData {
const self: *MetNo = @ptrCast(@alignCast(ptr));
const coords = try parseLocation(location);
// 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,
@ -33,6 +34,7 @@ pub const MetNo = struct {
);
defer self.allocator.free(url);
// Fetch weather data from met.no API
var client = std.http.Client{ .allocator = self.allocator };
defer client.deinit();
@ -45,7 +47,7 @@ pub const MetNo = struct {
.method = .GET,
.response_writer = &writer,
.extra_headers = &.{
.{ .name = "User-Agent", .value = "wttr.in/1.0" },
.{ .name = "User-Agent", .value = "wttr.in-zig/1.0 github.com/chubin/wttr.in" },
},
});
@ -55,15 +57,16 @@ pub const MetNo = struct {
const response_body = response_buf[0..writer.end];
const json_data = try std.json.parseFromSlice(
// Parse JSON response
const parsed = try std.json.parseFromSlice(
std.json.Value,
self.allocator,
allocator,
response_body,
.{},
);
defer json_data.deinit();
defer parsed.deinit();
return try parseMetNoResponse(allocator, location, json_data.value);
return try parseMetNoResponse(allocator, location, parsed.value);
}
fn deinitProvider(ptr: *anyopaque) void {
@ -83,15 +86,14 @@ const Coords = struct {
fn parseLocation(location: []const u8) !Coords {
if (std.mem.indexOf(u8, location, ",")) |comma_idx| {
const lat_str = location[0..comma_idx];
const lon_str = location[comma_idx + 1 ..];
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 Coords{ .lat = 51.5074, .lon = -0.1278 };
return error.InvalidLocationFormat;
}
fn parseMetNoResponse(allocator: std.mem.Allocator, location: []const u8, json: std.json.Value) !types.WeatherData {
@ -107,25 +109,34 @@ fn parseMetNoResponse(allocator: std.mem.Allocator, location: []const u8, json:
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_kph = @as(f32, @floatCast(details.object.get("wind_speed").?.float * 3.6));
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, symbol_code),
.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, "N"),
.wind_dir = try allocator.dupe(u8, wind_dir),
.pressure_mb = pressure_mb,
.precip_mm = 0.0,
},
@ -146,3 +157,58 @@ fn symbolCodeToWeatherCode(symbol: []const u8) u16 {
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"));
}