Files
weather-or-not/src/lib/activities.ts

191 lines
6.5 KiB
TypeScript

import { WeatherData, ActivityResult, RuleResult, ActivityConfig } from '@/types';
import { getWeatherDescription } from './weather';
import { daysSinceLastActivity } from './storage';
export function evaluateMotorcycle(
weather: WeatherData,
config: ActivityConfig,
): ActivityResult {
const rules: RuleResult[] = [];
const reasoning: string[] = [];
// Temperature
const temp = weather.current.temperature_2m;
const minTemp = config.min_temp_c ?? 10;
const maxTemp = config.max_temp_c ?? 35;
if (temp < minTemp) {
rules.push({ name: 'Temperature', passed: false, message: `${temp}°C is below minimum ${minTemp}°C` });
reasoning.push(`Too cold: ${temp}°C < ${minTemp}°C minimum`);
} else if (temp > maxTemp) {
rules.push({ name: 'Temperature', passed: false, message: `${temp}°C exceeds maximum ${maxTemp}°C` });
reasoning.push(`Too hot: ${temp}°C > ${maxTemp}°C maximum`);
} else {
rules.push({ name: 'Temperature', passed: true, message: `${temp}°C is comfortable` });
}
// Precipitation
const precip = weather.daily.precipitation_sum[0] ?? 0;
const maxPrecip = config.max_precipitation_mm ?? 0;
if (precip > maxPrecip) {
rules.push({ name: 'Precipitation', passed: false, message: `${precip}mm expected today` });
reasoning.push(`Rain expected: ${precip}mm`);
} else {
rules.push({ name: 'Precipitation', passed: true, message: 'No rain expected' });
}
// Wind
const wind = weather.current.wind_speed_10m;
const maxWind = config.max_wind_speed_kmh ?? 30;
if (wind > maxWind) {
rules.push({ name: 'Wind', passed: false, message: `${wind} km/h exceeds safe limit` });
reasoning.push(`Wind too strong: ${wind} km/h`);
} else {
rules.push({ name: 'Wind', passed: true, message: `${wind} km/h is safe` });
}
// Humidity
const humidity = weather.current.relative_humidity_2m;
const maxHum = config.max_humidity_pct ?? 90;
if (humidity > maxHum) {
rules.push({ name: 'Humidity', passed: false, message: `${humidity}% is uncomfortable` });
reasoning.push(`High humidity: ${humidity}%`);
} else {
rules.push({ name: 'Humidity', passed: true, message: `${humidity}% is comfortable` });
}
// Weather code (snow, ice, thunderstorm)
const code = weather.current.weather_code;
const dangerousCodes = [45, 48, 66, 67, 71, 73, 75, 77, 85, 86, 95, 96, 99];
if (dangerousCodes.includes(code)) {
rules.push({ name: 'Weather', passed: false, message: getWeatherDescription(code) });
reasoning.push(getWeatherDescription(code));
} else {
rules.push({ name: 'Weather', passed: true, message: getWeatherDescription(code) });
}
const verdict = determineVerdict(rules);
return {
name: 'Motorcycle Ride',
verdict,
rules,
reasoning,
icon: '🏍️',
description: 'Check conditions for motorcycle commuting',
};
}
export function evaluateLawn(
weather: WeatherData,
config: ActivityConfig,
): ActivityResult {
const rules: RuleResult[] = [];
const reasoning: string[] = [];
// Cooldown check
const cooldownDays = config.cooldown_days ?? 3;
const daysSince = daysSinceLastActivity('mow_lawn');
if (daysSince < cooldownDays) {
rules.push({
name: 'Cooldown',
passed: false,
message: `Last mowed ${daysSince} day(s) ago (wait ${cooldownDays - daysSince} more)`,
});
reasoning.push(`Cooldown not met: ${daysSince} days since last mow (need ${cooldownDays})`);
} else {
rules.push({
name: 'Cooldown',
passed: true,
message: `Good — ${daysSince} days since last mow (minimum: ${cooldownDays})`,
});
}
// Today's precipitation
const precipToday = weather.daily.precipitation_sum[0] ?? 0;
const maxPrecip = config.max_precipitation_mm ?? 0;
if (precipToday > maxPrecip) {
rules.push({ name: 'Rain today', passed: false, message: `${precipToday}mm expected` });
reasoning.push(`Rain expected today: ${precipToday}mm`);
} else {
rules.push({ name: 'Rain today', passed: true, message: 'No rain expected' });
}
// Past 24h precipitation (ground dryness)
const precip24h = calculatePast24hPrecip(weather);
const max24h = config.max_precipitation_past_24h_mm ?? 5;
if (precip24h > max24h) {
rules.push({ name: 'Ground moisture', passed: false, message: `${precip24h}mm in past 24h` });
reasoning.push(`Ground too wet: ${precip24h}mm in past 24h`);
} else {
rules.push({ name: 'Ground moisture', passed: true, message: `Ground is dry (${precip24h}mm)` });
}
// Wind
const wind = weather.current.wind_speed_10m;
const maxWind = config.max_wind_speed_kmh ?? 25;
if (wind > maxWind) {
rules.push({ name: 'Wind', passed: false, message: `${wind} km/h exceeds limit` });
reasoning.push(`Wind too strong: ${wind} km/h`);
} else {
rules.push({ name: 'Wind', passed: true, message: `${wind} km/h is safe` });
}
// Temperature
const temp = weather.current.temperature_2m;
const minTemp = config.min_temp_c ?? 15;
if (temp < minTemp) {
rules.push({ name: 'Temperature', passed: false, message: `${temp}°C is below ${minTemp}°C` });
reasoning.push(`Too cold: ${temp}°C`);
} else {
rules.push({ name: 'Temperature', passed: true, message: `${temp}°C is comfortable` });
}
// Weather code
const code = weather.current.weather_code;
if (code <= 3) {
rules.push({ name: 'Weather', passed: true, message: getWeatherDescription(code) });
} else {
rules.push({ name: 'Weather', passed: false, message: getWeatherDescription(code) });
reasoning.push(getWeatherDescription(code));
}
const verdict = determineVerdict(rules);
return {
name: 'Mow Lawn',
verdict,
rules,
reasoning,
icon: '🌱',
description: 'Check if conditions are good for mowing',
};
}
function calculatePast24hPrecip(weather: WeatherData): number {
const now = new Date();
let total = 0;
const hourly = weather.hourly;
for (let i = 0; i < hourly.time.length; i++) {
const time = new Date(hourly.time[i]);
const diffHours = (now.getTime() - time.getTime()) / (1000 * 60 * 60);
if (diffHours >= 0 && diffHours <= 24) {
total += hourly.precipitation[i] ?? 0;
}
}
return Math.round(total * 10) / 10;
}
function determineVerdict(rules: RuleResult[]): 'GO' | 'NO-GO' | 'CAUTION' {
const failed = rules.filter(r => !r.passed);
if (failed.length === 0) return 'GO';
// Critical failures = rain/precipitation
const critical = failed.some(r =>
r.name.toLowerCase().includes('rain') ||
r.name.toLowerCase().includes('precip') ||
r.name.toLowerCase().includes('cooldown')
);
return critical ? 'NO-GO' : 'CAUTION';
}