From 62bae1fb990ca8ec8436fe4516734541936989d2 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 3 Jan 2026 12:23:38 -0800 Subject: [PATCH] complete mock implementation, add tests/fix forecast calc (part 1) --- src/tests/metno_test_data.json | 1 + src/weather/MetNo.zig | 73 +++++++++++++++++++++++----------- src/weather/Mock.zig | 29 +++++++++++--- src/weather/types.zig | 2 + 4 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 src/tests/metno_test_data.json diff --git a/src/tests/metno_test_data.json b/src/tests/metno_test_data.json new file mode 100644 index 0000000..bf9b7c2 --- /dev/null +++ b/src/tests/metno_test_data.json @@ -0,0 +1 @@ +{"type":"Feature","geometry":{"type":"Point","coordinates":[-122.3301,47.6038,12]},"properties":{"meta":{"updated_at":"2026-01-02T23:17:22Z","units":{"air_pressure_at_sea_level":"hPa","air_temperature":"celsius","cloud_area_fraction":"%","precipitation_amount":"mm","relative_humidity":"%","wind_from_direction":"degrees","wind_speed":"m/s"}},"timeseries":[{"time":"2026-01-02T23:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1009.6,"air_temperature":6.5,"cloud_area_fraction":100.0,"relative_humidity":95.7,"wind_from_direction":93.3,"wind_speed":1.6}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.2}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":2.2}}}},{"time":"2026-01-03T00:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1009.3,"air_temperature":6.5,"cloud_area_fraction":100.0,"relative_humidity":96.0,"wind_from_direction":114.5,"wind_speed":1.3}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":0.3}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":2.1}}}},{"time":"2026-01-03T01:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1008.5,"air_temperature":6.4,"cloud_area_fraction":100.0,"relative_humidity":95.1,"wind_from_direction":97.0,"wind_speed":2.1}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":0.6}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":1.8}}}},{"time":"2026-01-03T02:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1007.8,"air_temperature":6.7,"cloud_area_fraction":100.0,"relative_humidity":93.9,"wind_from_direction":92.7,"wind_speed":1.9}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":0.7}},"next_6_hours":{"summary":{"symbol_code":"rainshowers_night"},"details":{"precipitation_amount":1.2}}}},{"time":"2026-01-03T03:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1006.9,"air_temperature":6.6,"cloud_area_fraction":100.0,"relative_humidity":94.2,"wind_from_direction":52.0,"wind_speed":1.9}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":0.4}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.5}}}},{"time":"2026-01-03T04:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1006.1,"air_temperature":6.4,"cloud_area_fraction":100.0,"relative_humidity":94.7,"wind_from_direction":51.2,"wind_speed":2.3}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.1}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.1}}}},{"time":"2026-01-03T05:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1005.1,"air_temperature":6.1,"cloud_area_fraction":100.0,"relative_humidity":95.0,"wind_from_direction":19.3,"wind_speed":2.0}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.1}}}},{"time":"2026-01-03T06:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1004.2,"air_temperature":5.8,"cloud_area_fraction":100.0,"relative_humidity":96.2,"wind_from_direction":14.2,"wind_speed":2.6}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"lightrainshowers_night"},"details":{"precipitation_amount":0.6}}}},{"time":"2026-01-03T07:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1003.8,"air_temperature":5.5,"cloud_area_fraction":73.4,"relative_humidity":97.1,"wind_from_direction":32.5,"wind_speed":2.0}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"rainshowers_night"},"details":{"precipitation_amount":1.8}}}},{"time":"2026-01-03T08:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1002.9,"air_temperature":4.9,"cloud_area_fraction":25.0,"relative_humidity":98.3,"wind_from_direction":5.4,"wind_speed":2.6}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"fair_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":2.9}}}},{"time":"2026-01-03T09:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1002.3,"air_temperature":4.6,"cloud_area_fraction":84.4,"relative_humidity":97.3,"wind_from_direction":9.4,"wind_speed":2.3}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":3.7}}}},{"time":"2026-01-03T10:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1001.8,"air_temperature":4.8,"cloud_area_fraction":100.0,"relative_humidity":96.3,"wind_from_direction":34.9,"wind_speed":1.5}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.1}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":3.9}}}},{"time":"2026-01-03T11:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1002.2,"air_temperature":5.6,"cloud_area_fraction":100.0,"relative_humidity":93.5,"wind_from_direction":189.8,"wind_speed":1.9}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":0.5}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":3.8}}}},{"time":"2026-01-03T12:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1001.3,"air_temperature":6.9,"cloud_area_fraction":100.0,"relative_humidity":86.3,"wind_from_direction":148.8,"wind_speed":4.0}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"heavyrain"},"details":{"precipitation_amount":1.2}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":3.4}}}},{"time":"2026-01-03T13:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1001.3,"air_temperature":7.2,"cloud_area_fraction":100.0,"relative_humidity":89.5,"wind_from_direction":153.5,"wind_speed":4.5}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"heavyrain"},"details":{"precipitation_amount":1.1}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":2.2}}}},{"time":"2026-01-03T14:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1001.0,"air_temperature":7.4,"cloud_area_fraction":100.0,"relative_humidity":90.9,"wind_from_direction":145.9,"wind_speed":4.0}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":0.8}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":1.1}}}},{"time":"2026-01-03T15:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1000.7,"air_temperature":7.5,"cloud_area_fraction":100.0,"relative_humidity":91.4,"wind_from_direction":133.5,"wind_speed":4.1}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.2}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.4}}}},{"time":"2026-01-03T16:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1000.5,"air_temperature":7.2,"cloud_area_fraction":100.0,"relative_humidity":91.9,"wind_from_direction":149.5,"wind_speed":4.4}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.2}}}},{"time":"2026-01-03T17:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1001.1,"air_temperature":7.6,"cloud_area_fraction":100.0,"relative_humidity":89.3,"wind_from_direction":190.8,"wind_speed":3.6}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.1}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.2}}}},{"time":"2026-01-03T18:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1001.3,"air_temperature":7.6,"cloud_area_fraction":65.6,"relative_humidity":90.5,"wind_from_direction":157.9,"wind_speed":3.4}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.1}}}},{"time":"2026-01-03T19:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1000.4,"air_temperature":9.2,"cloud_area_fraction":99.2,"relative_humidity":91.0,"wind_from_direction":139.8,"wind_speed":3.6}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.1}}}},{"time":"2026-01-03T20:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":999.2,"air_temperature":10.5,"cloud_area_fraction":81.2,"relative_humidity":87.5,"wind_from_direction":215.7,"wind_speed":3.4}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.4}}}},{"time":"2026-01-03T21:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":998.4,"air_temperature":10.9,"cloud_area_fraction":100.0,"relative_humidity":84.8,"wind_from_direction":232.2,"wind_speed":3.1}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.4}}}},{"time":"2026-01-03T22:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":997.8,"air_temperature":11.2,"cloud_area_fraction":100.0,"relative_humidity":83.3,"wind_from_direction":221.2,"wind_speed":2.0}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.7}}}},{"time":"2026-01-03T23:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":997.0,"air_temperature":10.3,"cloud_area_fraction":100.0,"relative_humidity":88.1,"wind_from_direction":181.7,"wind_speed":1.7}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"heavyrain"},"details":{"precipitation_amount":5.2}}}},{"time":"2026-01-04T00:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":995.3,"air_temperature":9.7,"cloud_area_fraction":100.0,"relative_humidity":88.7,"wind_from_direction":52.0,"wind_speed":2.0}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"heavyrain"},"details":{"precipitation_amount":5.1}}}},{"time":"2026-01-04T01:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":994.9,"air_temperature":9.1,"cloud_area_fraction":100.0,"relative_humidity":88.6,"wind_from_direction":16.4,"wind_speed":2.7}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.2}},"next_6_hours":{"summary":{"symbol_code":"heavyrain"},"details":{"precipitation_amount":5.1}}}},{"time":"2026-01-04T02:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":993.2,"air_temperature":8.6,"cloud_area_fraction":100.0,"relative_humidity":88.5,"wind_from_direction":27.9,"wind_speed":4.2}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":4.9}}}},{"time":"2026-01-04T03:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":992.2,"air_temperature":8.5,"cloud_area_fraction":97.7,"relative_humidity":78.3,"wind_from_direction":100.2,"wind_speed":4.2}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":0.3}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":4.9}}}},{"time":"2026-01-04T04:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":992.6,"air_temperature":8.4,"cloud_area_fraction":98.4,"relative_humidity":76.5,"wind_from_direction":180.3,"wind_speed":4.2}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"heavyrain"},"details":{"precipitation_amount":4.5}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":4.5}}}},{"time":"2026-01-04T05:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":992.1,"air_temperature":7.0,"cloud_area_fraction":99.2,"relative_humidity":79.6,"wind_from_direction":136.2,"wind_speed":4.4}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.1}}}},{"time":"2026-01-04T06:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":992.7,"air_temperature":7.5,"cloud_area_fraction":100.0,"relative_humidity":79.9,"wind_from_direction":177.1,"wind_speed":3.8}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.2}}}},{"time":"2026-01-04T07:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":993.1,"air_temperature":7.3,"cloud_area_fraction":100.0,"relative_humidity":82.9,"wind_from_direction":171.1,"wind_speed":4.6}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.2}}}},{"time":"2026-01-04T08:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":993.7,"air_temperature":7.4,"cloud_area_fraction":64.8,"relative_humidity":79.4,"wind_from_direction":155.9,"wind_speed":5.2}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.3}}}},{"time":"2026-01-04T09:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":993.4,"air_temperature":6.9,"cloud_area_fraction":55.5,"relative_humidity":79.9,"wind_from_direction":152.4,"wind_speed":3.7}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.4}}}},{"time":"2026-01-04T10:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":993.7,"air_temperature":7.0,"cloud_area_fraction":100.0,"relative_humidity":80.9,"wind_from_direction":147.5,"wind_speed":3.7}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.4}}}},{"time":"2026-01-04T11:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":994.2,"air_temperature":7.9,"cloud_area_fraction":100.0,"relative_humidity":79.0,"wind_from_direction":166.5,"wind_speed":5.9}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.1}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.4}}}},{"time":"2026-01-04T12:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":994.5,"air_temperature":9.6,"cloud_area_fraction":96.9,"relative_humidity":75.7,"wind_from_direction":185.4,"wind_speed":8.6}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.3}}}},{"time":"2026-01-04T13:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":995.6,"air_temperature":9.6,"cloud_area_fraction":99.2,"relative_humidity":77.3,"wind_from_direction":193.3,"wind_speed":9.2}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.5}}}},{"time":"2026-01-04T14:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":996.6,"air_temperature":8.9,"cloud_area_fraction":39.1,"relative_humidity":80.4,"wind_from_direction":190.1,"wind_speed":8.8}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":1.5}}}},{"time":"2026-01-04T15:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":997.7,"air_temperature":8.7,"cloud_area_fraction":100.0,"relative_humidity":77.1,"wind_from_direction":192.9,"wind_speed":8.8}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":2.7}}}},{"time":"2026-01-04T16:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":998.7,"air_temperature":8.5,"cloud_area_fraction":100.0,"relative_humidity":76.8,"wind_from_direction":192.9,"wind_speed":8.0}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":3.0}}}},{"time":"2026-01-04T17:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":999.6,"air_temperature":8.6,"cloud_area_fraction":96.1,"relative_humidity":75.0,"wind_from_direction":185.5,"wind_speed":7.1}},"next_12_hours":{"summary":{"symbol_code":"lightrainshowers_day"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":3.1}}}},{"time":"2026-01-04T18:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1000.0,"air_temperature":9.0,"cloud_area_fraction":100.0,"relative_humidity":72.9,"wind_from_direction":182.5,"wind_speed":6.8}},"next_12_hours":{"summary":{"symbol_code":"lightrainshowers_day"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.2}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":3.1}}}},{"time":"2026-01-04T19:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1000.2,"air_temperature":9.2,"cloud_area_fraction":100.0,"relative_humidity":75.0,"wind_from_direction":186.7,"wind_speed":6.7}},"next_12_hours":{"summary":{"symbol_code":"lightrainshowers_day"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"heavyrain"},"details":{"precipitation_amount":1.2}},"next_6_hours":{"summary":{"symbol_code":"rainshowers_day"},"details":{"precipitation_amount":3.0}}}},{"time":"2026-01-04T20:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1000.1,"air_temperature":8.3,"cloud_area_fraction":100.0,"relative_humidity":79.8,"wind_from_direction":189.6,"wind_speed":6.3}},"next_12_hours":{"summary":{"symbol_code":"lightrainshowers_day"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"heavyrain"},"details":{"precipitation_amount":1.2}},"next_6_hours":{"summary":{"symbol_code":"rainshowers_day"},"details":{"precipitation_amount":1.9}}}},{"time":"2026-01-04T21:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":999.9,"air_temperature":7.6,"cloud_area_fraction":100.0,"relative_humidity":86.7,"wind_from_direction":180.3,"wind_speed":7.5}},"next_12_hours":{"summary":{"symbol_code":"lightrainshowers_day"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":0.4}},"next_6_hours":{"summary":{"symbol_code":"lightrainshowers_day"},"details":{"precipitation_amount":0.8}}}},{"time":"2026-01-04T22:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1000.0,"air_temperature":7.9,"cloud_area_fraction":100.0,"relative_humidity":83.8,"wind_from_direction":178.8,"wind_speed":8.3}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.2}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{"precipitation_amount":0.5}}}},{"time":"2026-01-04T23:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1000.5,"air_temperature":8.0,"cloud_area_fraction":100.0,"relative_humidity":81.5,"wind_from_direction":180.4,"wind_speed":8.0}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.3}}}},{"time":"2026-01-05T00:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1000.9,"air_temperature":7.9,"cloud_area_fraction":46.1,"relative_humidity":79.9,"wind_from_direction":181.7,"wind_speed":7.1}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.3}}}},{"time":"2026-01-05T01:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1001.5,"air_temperature":7.4,"cloud_area_fraction":62.5,"relative_humidity":80.4,"wind_from_direction":194.1,"wind_speed":7.3}},"next_1_hours":{"summary":{"symbol_code":"lightrainshowers_night"},"details":{"precipitation_amount":0.1}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.3}}}},{"time":"2026-01-05T02:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1002.0,"air_temperature":7.0,"cloud_area_fraction":93.0,"relative_humidity":80.9,"wind_from_direction":204.2,"wind_speed":7.6}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.2}}}},{"time":"2026-01-05T03:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1002.6,"air_temperature":5.8,"cloud_area_fraction":68.7,"relative_humidity":87.3,"wind_from_direction":200.3,"wind_speed":6.6}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.2}}}},{"time":"2026-01-05T04:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1003.4,"air_temperature":5.9,"cloud_area_fraction":76.6,"relative_humidity":88.0,"wind_from_direction":199.5,"wind_speed":6.2}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.1}}}},{"time":"2026-01-05T05:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1004.3,"air_temperature":5.0,"cloud_area_fraction":33.6,"relative_humidity":90.8,"wind_from_direction":187.9,"wind_speed":7.0}},"next_1_hours":{"summary":{"symbol_code":"fair_night"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.1}}}},{"time":"2026-01-05T06:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1005.2,"air_temperature":5.0,"cloud_area_fraction":100.0,"relative_humidity":91.7,"wind_from_direction":188.3,"wind_speed":6.9}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.1}}}},{"time":"2026-01-05T07:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1006.2,"air_temperature":5.0,"cloud_area_fraction":100.0,"relative_humidity":91.1,"wind_from_direction":192.7,"wind_speed":7.0}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-05T08:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1006.7,"air_temperature":5.2,"cloud_area_fraction":99.2,"relative_humidity":90.2,"wind_from_direction":195.8,"wind_speed":6.3}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-05T09:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1007.5,"air_temperature":5.2,"cloud_area_fraction":96.1,"relative_humidity":90.4,"wind_from_direction":200.8,"wind_speed":5.9}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-05T10:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1008.3,"air_temperature":5.0,"cloud_area_fraction":89.8,"relative_humidity":90.7,"wind_from_direction":199.3,"wind_speed":5.6}},"next_1_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-05T11:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1009.1,"air_temperature":4.6,"cloud_area_fraction":79.7,"relative_humidity":91.2,"wind_from_direction":200.4,"wind_speed":5.2}},"next_1_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-05T12:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1009.6,"air_temperature":4.2,"cloud_area_fraction":25.0,"relative_humidity":91.9,"wind_from_direction":198.6,"wind_speed":5.0}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"fair_day"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-05T18:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1014.5,"air_temperature":4.2,"cloud_area_fraction":93.7,"relative_humidity":91.0,"wind_from_direction":212.1,"wind_speed":3.0}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-06T00:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1015.9,"air_temperature":5.7,"cloud_area_fraction":43.0,"relative_humidity":83.3,"wind_from_direction":79.1,"wind_speed":0.8}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-06T06:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1016.2,"air_temperature":3.5,"cloud_area_fraction":100.0,"relative_humidity":91.4,"wind_from_direction":174.8,"wind_speed":3.7}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.2}}}},{"time":"2026-01-06T12:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1013.2,"air_temperature":4.6,"cloud_area_fraction":100.0,"relative_humidity":85.6,"wind_from_direction":177.3,"wind_speed":4.2}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":1.5}}}},{"time":"2026-01-06T18:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1008.5,"air_temperature":5.7,"cloud_area_fraction":100.0,"relative_humidity":82.0,"wind_from_direction":192.5,"wind_speed":8.4}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"heavyrain"},"details":{"precipitation_amount":8.4}}}},{"time":"2026-01-07T00:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1003.3,"air_temperature":5.3,"cloud_area_fraction":100.0,"relative_humidity":92.3,"wind_from_direction":205.4,"wind_speed":8.8}},"next_12_hours":{"summary":{"symbol_code":"lightrainshowers_night"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":3.6}}}},{"time":"2026-01-07T06:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1004.5,"air_temperature":4.9,"cloud_area_fraction":72.7,"relative_humidity":94.8,"wind_from_direction":199.3,"wind_speed":4.0}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-07T12:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1007.5,"air_temperature":3.1,"cloud_area_fraction":14.1,"relative_humidity":92.5,"wind_from_direction":193.7,"wind_speed":3.5}},"next_12_hours":{"summary":{"symbol_code":"rainshowers_day"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"fair_day"},"details":{"precipitation_amount":0.4}}}},{"time":"2026-01-07T18:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1008.0,"air_temperature":4.8,"cloud_area_fraction":100.0,"relative_humidity":82.3,"wind_from_direction":180.1,"wind_speed":6.8}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"heavyrain"},"details":{"precipitation_amount":6.2}}}},{"time":"2026-01-08T00:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1003.7,"air_temperature":4.4,"cloud_area_fraction":100.0,"relative_humidity":89.9,"wind_from_direction":206.0,"wind_speed":6.3}},"next_12_hours":{"summary":{"symbol_code":"lightrainshowers_night"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":1.2}}}},{"time":"2026-01-08T06:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1010.0,"air_temperature":3.2,"cloud_area_fraction":93.7,"relative_humidity":92.8,"wind_from_direction":183.4,"wind_speed":3.8}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-08T12:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1015.6,"air_temperature":2.6,"cloud_area_fraction":32.0,"relative_humidity":91.7,"wind_from_direction":192.3,"wind_speed":3.2}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"fair_day"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-08T18:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1022.1,"air_temperature":3.6,"cloud_area_fraction":100.0,"relative_humidity":87.2,"wind_from_direction":191.8,"wind_speed":4.0}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-09T00:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1025.6,"air_temperature":6.1,"cloud_area_fraction":96.9,"relative_humidity":77.5,"wind_from_direction":111.2,"wind_speed":1.3}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-09T06:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1029.4,"air_temperature":3.3,"cloud_area_fraction":78.9,"relative_humidity":88.5,"wind_from_direction":181.0,"wind_speed":2.5}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{"precipitation_amount":0.1}}}},{"time":"2026-01-09T12:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1031.1,"air_temperature":4.2,"cloud_area_fraction":97.7,"relative_humidity":85.5,"wind_from_direction":158.5,"wind_speed":2.0}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-09T18:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1032.9,"air_temperature":4.9,"cloud_area_fraction":100.0,"relative_humidity":82.8,"wind_from_direction":163.9,"wind_speed":2.5}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-10T00:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1032.7,"air_temperature":7.0,"cloud_area_fraction":97.7,"relative_humidity":79.4,"wind_from_direction":157.2,"wind_speed":1.6}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-10T06:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1031.8,"air_temperature":2.6,"cloud_area_fraction":98.4,"relative_humidity":95.0,"wind_from_direction":157.6,"wind_speed":1.1}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-10T12:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1030.3,"air_temperature":2.7,"cloud_area_fraction":99.2,"relative_humidity":85.8,"wind_from_direction":165.0,"wind_speed":1.9}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"lightrain"},"details":{"precipitation_amount":0.9}}}},{"time":"2026-01-10T18:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1032.3,"air_temperature":5.3,"cloud_area_fraction":100.0,"relative_humidity":65.0,"wind_from_direction":103.0,"wind_speed":2.2}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":4.4}}}},{"time":"2026-01-11T00:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1031.0,"air_temperature":5.9,"cloud_area_fraction":100.0,"relative_humidity":76.0,"wind_from_direction":154.4,"wind_speed":3.1}},"next_12_hours":{"summary":{"symbol_code":"rain"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":3.1}}}},{"time":"2026-01-11T06:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1031.3,"air_temperature":5.7,"cloud_area_fraction":100.0,"relative_humidity":85.6,"wind_from_direction":146.8,"wind_speed":2.9}},"next_12_hours":{"summary":{"symbol_code":"lightrain"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"rain"},"details":{"precipitation_amount":1.3}}}},{"time":"2026-01-11T12:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1030.7,"air_temperature":5.3,"cloud_area_fraction":100.0,"relative_humidity":90.7,"wind_from_direction":134.0,"wind_speed":1.9}},"next_12_hours":{"summary":{"symbol_code":"cloudy"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.3}}}},{"time":"2026-01-11T18:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1031.5,"air_temperature":6.6,"cloud_area_fraction":100.0,"relative_humidity":88.8,"wind_from_direction":150.5,"wind_speed":2.8}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_day"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-12T00:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1030.3,"air_temperature":9.7,"cloud_area_fraction":100.0,"relative_humidity":83.2,"wind_from_direction":80.5,"wind_speed":0.1}},"next_12_hours":{"summary":{"symbol_code":"partlycloudy_night"},"details":{}},"next_6_hours":{"summary":{"symbol_code":"cloudy"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-12T06:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1030.8,"air_temperature":3.9,"cloud_area_fraction":1.6,"relative_humidity":91.9,"wind_from_direction":53.9,"wind_speed":1.0}},"next_6_hours":{"summary":{"symbol_code":"clearsky_night"},"details":{"precipitation_amount":0.0}}}},{"time":"2026-01-12T12:00:00Z","data":{"instant":{"details":{"air_pressure_at_sea_level":1030.9,"air_temperature":2.0,"cloud_area_fraction":67.2,"relative_humidity":95.4,"wind_from_direction":164.4,"wind_speed":1.2}}}}]}} diff --git a/src/weather/MetNo.zig b/src/weather/MetNo.zig index 8f1ed25..a0be8c4 100644 --- a/src/weather/MetNo.zig +++ b/src/weather/MetNo.zig @@ -116,7 +116,7 @@ fn fetchRaw(ptr: *anyopaque, allocator: std.mem.Allocator, coords: Coordinates) return try allocator.dupe(u8, response_body); } -fn parse(ptr: *anyopaque, allocator: std.mem.Allocator, raw: []const u8) !types.WeatherData { +pub fn parse(ptr: *anyopaque, allocator: std.mem.Allocator, raw: []const u8) !types.WeatherData { _ = ptr; // Parse JSON response @@ -268,31 +268,29 @@ fn parseForecastDays(allocator: std.mem.Allocator, timeseries: []std.json.Value) break :blk hrs; } - clearHourlyForecast(allocator, &day_all_hours); - - if (day_all_hours.items.len < 4) + if (day_all_hours.items.len < 4) { + clearHourlyForecast(allocator, &day_hourly); 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); + // Pick 4 evenly spaced entries from day_all_hours + 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(); + clearHourlyForecast(allocator, &day_hourly); + break :blk hrs; }; try days.append(allocator, .{ @@ -310,6 +308,7 @@ fn parseForecastDays(allocator: std.mem.Allocator, timeseries: []std.json.Value) // Start new day current_date = date; day_temps.clearRetainingCapacity(); + day_hourly.clearRetainingCapacity(); day_all_hours.clearRetainingCapacity(); day_symbol = null; } @@ -476,3 +475,29 @@ test "parseForecastDays handles empty timeseries" { defer allocator.free(forecast); try std.testing.expectEqual(@as(usize, 0), forecast.len); } + +test "hourly forecasts should have 4 entries per day" { + const allocator = std.testing.allocator; + + const json_data = @embedFile("../tests/metno_test_data.json"); + const weather_data = try parse(undefined, allocator, json_data); + defer weather_data.deinit(); + + // Skip first day if incomplete, check remaining days have 4 hourly entries + var checked: usize = 0; + for (weather_data.forecast) |day| { + if (day.hourly.len < 4) continue; // Skip incomplete days + + try std.testing.expectEqual(@as(usize, 4), day.hourly.len); + + // None should be "Unknown" + for (day.hourly) |hour| { + try std.testing.expect(!std.mem.eql(u8, hour.condition, "Unknown")); + } + + checked += 1; + if (checked >= 3) break; // Check 3 complete days + } + + try std.testing.expect(checked >= 2); // At least 2 complete days +} diff --git a/src/weather/Mock.zig b/src/weather/Mock.zig index f8f1a0a..6467344 100644 --- a/src/weather/Mock.zig +++ b/src/weather/Mock.zig @@ -8,11 +8,13 @@ const Mock = @This(); allocator: std.mem.Allocator, responses: std.StringHashMap([]const u8), +parse_fn: ?*const fn (ptr: *anyopaque, allocator: std.mem.Allocator, raw: []const u8) anyerror!types.WeatherData = null, pub fn init(allocator: std.mem.Allocator) !Mock { return Mock{ .allocator = allocator, .responses = std.StringHashMap([]const u8).init(allocator), + .parse_fn = null, }; } @@ -43,9 +45,10 @@ fn fetchRaw(ptr: *anyopaque, allocator: std.mem.Allocator, coords: Coordinates) } fn parse(ptr: *anyopaque, allocator: std.mem.Allocator, raw: []const u8) !types.WeatherData { - _ = ptr; - _ = allocator; - _ = raw; + const self: *Mock = @ptrCast(@alignCast(ptr)); + if (self.parse_fn) |parse_fn| { + return parse_fn(ptr, allocator, raw); + } return error.NotImplemented; } @@ -63,7 +66,21 @@ pub fn deinit(self: *Mock) void { self.responses.deinit(); } -test "mock weather provider" { - // TODO: Implement Mock.parse to enable this test - return error.SkipZigTest; +test "mock weather provider with MetNo parse" { + const MetNo = @import("MetNo.zig"); + const allocator = std.testing.allocator; + + const test_data = @embedFile("../tests/metno_test_data.json"); + + var mock = try Mock.init(allocator); + defer mock.deinit(); + + mock.parse_fn = MetNo.parse; + + // Parse directly - no fetching + const weather = try MetNo.parse(&mock, allocator, test_data); + defer weather.deinit(); + + // Verify we got valid weather data + try std.testing.expect(weather.forecast.len > 0); } diff --git a/src/weather/types.zig b/src/weather/types.zig index eed03e5..60ac855 100644 --- a/src/weather/types.zig +++ b/src/weather/types.zig @@ -89,6 +89,8 @@ pub const WeatherData = struct { pub fn deinit(self: WeatherData) void { self.allocator.free(self.location); + self.allocator.free(self.current.condition); + self.allocator.free(self.current.wind_dir); for (self.forecast) |day| { self.allocator.free(day.date); self.allocator.free(day.condition);