191 lines
6.5 KiB
TypeScript
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';
|
|
}
|