Rewrite as Next.js app with Livonia, MI location
This commit is contained in:
190
src/lib/activities.ts
Normal file
190
src/lib/activities.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
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';
|
||||
}
|
||||
Reference in New Issue
Block a user