feat: add wind speed and a same-day hive-inspection verdict to weather
Temperature and rain were already tracked, but wind matters just as much for whether it's safe/sensible to open a hive - bees get more defensive and frames are harder to handle in wind. Adds wind_speed_10m_max to the Open-Meteo request and a derived inspectionSuitable flag per day (>=15C, <=1mm rain, <=25km/h wind; null when a value is missing rather than guessing). The apiary weather card now shows a clear "good/not good for a Durchsicht today" banner plus wind alongside each day's forecast. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
1f67cd5548
commit
c4f970f071
4 files changed
+150
-29
No files matched your search
@@ -8,6 +8,7 @@ function openMeteoResponse(
|
||||
tempMax: number | null;
|
||||
tempMin: number | null;
|
||||
precipitation: number | null;
|
||||
wind?: number | null;
|
||||
}[],
|
||||
): unknown {
|
||||
return {
|
||||
@@ -16,6 +17,7 @@ function openMeteoResponse(
|
||||
temperature_2m_max: days.map((day) => day.tempMax),
|
||||
temperature_2m_min: days.map((day) => day.tempMin),
|
||||
precipitation_sum: days.map((day) => day.precipitation),
|
||||
wind_speed_10m_max: days.map((day) => day.wind ?? 10),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -83,13 +85,68 @@ describe('WeatherService', () => {
|
||||
const result = await weatherService.getWeather('apiary-1');
|
||||
|
||||
expect(result.daily).toEqual([
|
||||
{ date: '2026-06-01', tempMaxC: 22, tempMinC: 12, precipitationMm: 0 },
|
||||
{
|
||||
date: '2026-06-01',
|
||||
tempMaxC: 22,
|
||||
tempMinC: 12,
|
||||
precipitationMm: 0,
|
||||
windSpeedMaxKmh: 10,
|
||||
inspectionSuitable: true,
|
||||
},
|
||||
]);
|
||||
const [url] = fetchMock.mock.calls[0] as [URL];
|
||||
expect(url.toString()).toContain('latitude=52.5');
|
||||
expect(url.toString()).toContain('longitude=13.4');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['too cold', { tempMax: 10, tempMin: 4, precipitation: 0, wind: 5 }],
|
||||
['raining', { tempMax: 20, tempMin: 12, precipitation: 3, wind: 5 }],
|
||||
['too windy', { tempMax: 20, tempMin: 12, precipitation: 0, wind: 40 }],
|
||||
])('marks a day unsuitable for inspection when %s', async (_label, day) => {
|
||||
prisma.apiary.findUnique.mockResolvedValue({
|
||||
latitude: 52.5,
|
||||
longitude: 13.4,
|
||||
});
|
||||
fetchMock.mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify(openMeteoResponse([{ date: '2026-06-01', ...day }])),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
|
||||
const result = await weatherService.getWeather('apiary-1');
|
||||
|
||||
expect(result.daily[0].inspectionSuitable).toBe(false);
|
||||
});
|
||||
|
||||
it('leaves inspectionSuitable null when a required value is missing', async () => {
|
||||
prisma.apiary.findUnique.mockResolvedValue({
|
||||
latitude: 52.5,
|
||||
longitude: 13.4,
|
||||
});
|
||||
fetchMock.mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify(
|
||||
openMeteoResponse([
|
||||
{
|
||||
date: '2026-06-01',
|
||||
tempMax: null,
|
||||
tempMin: null,
|
||||
precipitation: 0,
|
||||
wind: 5,
|
||||
},
|
||||
]),
|
||||
),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
|
||||
const result = await weatherService.getWeather('apiary-1');
|
||||
|
||||
expect(result.daily[0].inspectionSuitable).toBeNull();
|
||||
});
|
||||
|
||||
it('throws BadRequestException when Open-Meteo responds with an error status', async () => {
|
||||
prisma.apiary.findUnique.mockResolvedValue({
|
||||
latitude: 52.5,
|
||||
|
||||
Reference in new issue
Block a user