Initial Claude Run
This commit is contained in:
454
backend/src/routes/weather.js
Normal file
454
backend/src/routes/weather.js
Normal file
@@ -0,0 +1,454 @@
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const pool = require('../config/database');
|
||||
const { AppError } = require('../middleware/errorHandler');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// @route GET /api/weather/:propertyId
|
||||
// @desc Get current weather for property location
|
||||
// @access Private
|
||||
router.get('/:propertyId', async (req, res, next) => {
|
||||
try {
|
||||
const propertyId = req.params.propertyId;
|
||||
|
||||
// Verify property belongs to user
|
||||
const propertyResult = await pool.query(
|
||||
'SELECT id, name, latitude, longitude FROM properties WHERE id = $1 AND user_id = $2',
|
||||
[propertyId, req.user.id]
|
||||
);
|
||||
|
||||
if (propertyResult.rows.length === 0) {
|
||||
throw new AppError('Property not found', 404);
|
||||
}
|
||||
|
||||
const property = propertyResult.rows[0];
|
||||
|
||||
if (!property.latitude || !property.longitude) {
|
||||
throw new AppError('Property location coordinates not set', 400);
|
||||
}
|
||||
|
||||
const apiKey = process.env.WEATHER_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new AppError('Weather service not configured', 503);
|
||||
}
|
||||
|
||||
// Fetch current weather from OpenWeatherMap
|
||||
const weatherResponse = await axios.get(
|
||||
`https://api.openweathermap.org/data/2.5/weather`,
|
||||
{
|
||||
params: {
|
||||
lat: property.latitude,
|
||||
lon: property.longitude,
|
||||
appid: apiKey,
|
||||
units: 'imperial'
|
||||
},
|
||||
timeout: 5000
|
||||
}
|
||||
);
|
||||
|
||||
const weatherData = weatherResponse.data;
|
||||
|
||||
// Store weather data in cache
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
await pool.query(
|
||||
`INSERT INTO weather_data
|
||||
(property_id, date, temperature_high, temperature_low, humidity, wind_speed, precipitation, conditions)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (property_id, date)
|
||||
DO UPDATE SET
|
||||
temperature_high = EXCLUDED.temperature_high,
|
||||
temperature_low = EXCLUDED.temperature_low,
|
||||
humidity = EXCLUDED.humidity,
|
||||
wind_speed = EXCLUDED.wind_speed,
|
||||
precipitation = EXCLUDED.precipitation,
|
||||
conditions = EXCLUDED.conditions,
|
||||
created_at = CURRENT_TIMESTAMP`,
|
||||
[
|
||||
propertyId,
|
||||
today,
|
||||
Math.round(weatherData.main.temp),
|
||||
Math.round(weatherData.main.temp_min),
|
||||
weatherData.main.humidity,
|
||||
weatherData.wind?.speed || 0,
|
||||
weatherData.rain?.['1h'] || 0,
|
||||
weatherData.weather[0].description
|
||||
]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
weather: {
|
||||
location: {
|
||||
propertyId: property.id,
|
||||
propertyName: property.name,
|
||||
latitude: parseFloat(property.latitude),
|
||||
longitude: parseFloat(property.longitude)
|
||||
},
|
||||
current: {
|
||||
temperature: Math.round(weatherData.main.temp),
|
||||
feelsLike: Math.round(weatherData.main.feels_like),
|
||||
humidity: weatherData.main.humidity,
|
||||
pressure: weatherData.main.pressure,
|
||||
windSpeed: weatherData.wind?.speed || 0,
|
||||
windDirection: weatherData.wind?.deg || 0,
|
||||
visibility: weatherData.visibility ? weatherData.visibility / 1609.34 : null, // Convert to miles
|
||||
uvIndex: weatherData.uvi || null,
|
||||
conditions: weatherData.weather[0].description,
|
||||
icon: weatherData.weather[0].icon
|
||||
},
|
||||
precipitation: {
|
||||
current: weatherData.rain?.['1h'] || 0,
|
||||
forecast3h: weatherData.rain?.['3h'] || 0
|
||||
},
|
||||
timestamps: {
|
||||
sunrise: new Date(weatherData.sys.sunrise * 1000),
|
||||
sunset: new Date(weatherData.sys.sunset * 1000),
|
||||
lastUpdated: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
|
||||
throw new AppError('Weather service temporarily unavailable', 503);
|
||||
}
|
||||
if (error.response?.status === 401) {
|
||||
throw new AppError('Weather service authentication failed', 503);
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/weather/:propertyId/forecast
|
||||
// @desc Get 5-day weather forecast for property
|
||||
// @access Private
|
||||
router.get('/:propertyId/forecast', async (req, res, next) => {
|
||||
try {
|
||||
const propertyId = req.params.propertyId;
|
||||
|
||||
// Verify property belongs to user
|
||||
const propertyResult = await pool.query(
|
||||
'SELECT id, name, latitude, longitude FROM properties WHERE id = $1 AND user_id = $2',
|
||||
[propertyId, req.user.id]
|
||||
);
|
||||
|
||||
if (propertyResult.rows.length === 0) {
|
||||
throw new AppError('Property not found', 404);
|
||||
}
|
||||
|
||||
const property = propertyResult.rows[0];
|
||||
|
||||
if (!property.latitude || !property.longitude) {
|
||||
throw new AppError('Property location coordinates not set', 400);
|
||||
}
|
||||
|
||||
const apiKey = process.env.WEATHER_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new AppError('Weather service not configured', 503);
|
||||
}
|
||||
|
||||
// Fetch 5-day forecast from OpenWeatherMap
|
||||
const forecastResponse = await axios.get(
|
||||
`https://api.openweathermap.org/data/2.5/forecast`,
|
||||
{
|
||||
params: {
|
||||
lat: property.latitude,
|
||||
lon: property.longitude,
|
||||
appid: apiKey,
|
||||
units: 'imperial'
|
||||
},
|
||||
timeout: 5000
|
||||
}
|
||||
);
|
||||
|
||||
const forecastData = forecastResponse.data;
|
||||
|
||||
// Group forecast by day
|
||||
const dailyForecast = {};
|
||||
|
||||
forecastData.list.forEach(item => {
|
||||
const date = item.dt_txt.split(' ')[0];
|
||||
|
||||
if (!dailyForecast[date]) {
|
||||
dailyForecast[date] = {
|
||||
date,
|
||||
temperatures: [],
|
||||
humidity: [],
|
||||
windSpeed: [],
|
||||
precipitation: 0,
|
||||
conditions: [],
|
||||
timestamps: []
|
||||
};
|
||||
}
|
||||
|
||||
dailyForecast[date].temperatures.push(item.main.temp);
|
||||
dailyForecast[date].humidity.push(item.main.humidity);
|
||||
dailyForecast[date].windSpeed.push(item.wind?.speed || 0);
|
||||
dailyForecast[date].precipitation += item.rain?.['3h'] || 0;
|
||||
dailyForecast[date].conditions.push(item.weather[0].description);
|
||||
dailyForecast[date].timestamps.push(new Date(item.dt * 1000));
|
||||
});
|
||||
|
||||
// Process daily summaries
|
||||
const forecast = Object.values(dailyForecast).map(day => {
|
||||
const temps = day.temperatures;
|
||||
const humidity = day.humidity;
|
||||
const windSpeeds = day.windSpeed;
|
||||
|
||||
return {
|
||||
date: day.date,
|
||||
temperatureHigh: Math.round(Math.max(...temps)),
|
||||
temperatureLow: Math.round(Math.min(...temps)),
|
||||
averageHumidity: Math.round(humidity.reduce((a, b) => a + b, 0) / humidity.length),
|
||||
maxWindSpeed: Math.round(Math.max(...windSpeeds)),
|
||||
totalPrecipitation: Math.round(day.precipitation * 100) / 100,
|
||||
conditions: day.conditions[0], // Use first condition of the day
|
||||
timestamps: day.timestamps
|
||||
};
|
||||
}).slice(0, 5); // Limit to 5 days
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
forecast: {
|
||||
location: {
|
||||
propertyId: property.id,
|
||||
propertyName: property.name,
|
||||
latitude: parseFloat(property.latitude),
|
||||
longitude: parseFloat(property.longitude)
|
||||
},
|
||||
daily: forecast,
|
||||
lastUpdated: new Date()
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
|
||||
throw new AppError('Weather service temporarily unavailable', 503);
|
||||
}
|
||||
if (error.response?.status === 401) {
|
||||
throw new AppError('Weather service authentication failed', 503);
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/weather/:propertyId/history
|
||||
// @desc Get weather history for property
|
||||
// @access Private
|
||||
router.get('/:propertyId/history', async (req, res, next) => {
|
||||
try {
|
||||
const propertyId = req.params.propertyId;
|
||||
const { start_date, end_date, limit = 30 } = req.query;
|
||||
|
||||
// Verify property belongs to user
|
||||
const propertyResult = await pool.query(
|
||||
'SELECT id, name FROM properties WHERE id = $1 AND user_id = $2',
|
||||
[propertyId, req.user.id]
|
||||
);
|
||||
|
||||
if (propertyResult.rows.length === 0) {
|
||||
throw new AppError('Property not found', 404);
|
||||
}
|
||||
|
||||
const property = propertyResult.rows[0];
|
||||
|
||||
let whereConditions = ['property_id = $1'];
|
||||
let queryParams = [propertyId];
|
||||
let paramCount = 1;
|
||||
|
||||
if (start_date) {
|
||||
paramCount++;
|
||||
whereConditions.push(`date >= $${paramCount}`);
|
||||
queryParams.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
paramCount++;
|
||||
whereConditions.push(`date <= $${paramCount}`);
|
||||
queryParams.push(end_date);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
paramCount++;
|
||||
queryParams.push(limit);
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM weather_data
|
||||
WHERE ${whereClause}
|
||||
ORDER BY date DESC
|
||||
LIMIT $${paramCount}`,
|
||||
queryParams
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
history: {
|
||||
location: {
|
||||
propertyId: property.id,
|
||||
propertyName: property.name
|
||||
},
|
||||
records: result.rows.map(record => ({
|
||||
date: record.date,
|
||||
temperatureHigh: record.temperature_high,
|
||||
temperatureLow: record.temperature_low,
|
||||
humidity: record.humidity,
|
||||
windSpeed: parseFloat(record.wind_speed),
|
||||
precipitation: parseFloat(record.precipitation),
|
||||
conditions: record.conditions,
|
||||
recordedAt: record.created_at
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/weather/conditions/suitable
|
||||
// @desc Check if weather conditions are suitable for application
|
||||
// @access Private
|
||||
router.get('/conditions/suitable/:propertyId', async (req, res, next) => {
|
||||
try {
|
||||
const propertyId = req.params.propertyId;
|
||||
const { application_type = 'general' } = req.query;
|
||||
|
||||
// Verify property belongs to user
|
||||
const propertyResult = await pool.query(
|
||||
'SELECT id, name, latitude, longitude FROM properties WHERE id = $1 AND user_id = $2',
|
||||
[propertyId, req.user.id]
|
||||
);
|
||||
|
||||
if (propertyResult.rows.length === 0) {
|
||||
throw new AppError('Property not found', 404);
|
||||
}
|
||||
|
||||
const property = propertyResult.rows[0];
|
||||
|
||||
if (!property.latitude || !property.longitude) {
|
||||
throw new AppError('Property location coordinates not set', 400);
|
||||
}
|
||||
|
||||
const apiKey = process.env.WEATHER_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new AppError('Weather service not configured', 503);
|
||||
}
|
||||
|
||||
// Get current weather
|
||||
const weatherResponse = await axios.get(
|
||||
`https://api.openweathermap.org/data/2.5/weather`,
|
||||
{
|
||||
params: {
|
||||
lat: property.latitude,
|
||||
lon: property.longitude,
|
||||
appid: apiKey,
|
||||
units: 'imperial'
|
||||
},
|
||||
timeout: 5000
|
||||
}
|
||||
);
|
||||
|
||||
const weather = weatherResponse.data;
|
||||
|
||||
// Define suitability criteria based on application type
|
||||
const criteria = {
|
||||
general: {
|
||||
maxWindSpeed: 10, // mph
|
||||
maxTemperature: 85, // °F
|
||||
minTemperature: 45, // °F
|
||||
maxHumidity: 85, // %
|
||||
maxPrecipitation: 0.1 // inches
|
||||
},
|
||||
herbicide: {
|
||||
maxWindSpeed: 7,
|
||||
maxTemperature: 80,
|
||||
minTemperature: 50,
|
||||
maxHumidity: 80,
|
||||
maxPrecipitation: 0
|
||||
},
|
||||
fertilizer: {
|
||||
maxWindSpeed: 15,
|
||||
maxTemperature: 90,
|
||||
minTemperature: 40,
|
||||
maxHumidity: 90,
|
||||
maxPrecipitation: 0
|
||||
}
|
||||
};
|
||||
|
||||
const rules = criteria[application_type] || criteria.general;
|
||||
|
||||
// Check conditions
|
||||
const windSpeed = weather.wind?.speed || 0;
|
||||
const temperature = weather.main.temp;
|
||||
const humidity = weather.main.humidity;
|
||||
const precipitation = weather.rain?.['1h'] || 0;
|
||||
|
||||
const checks = {
|
||||
windSpeed: {
|
||||
suitable: windSpeed <= rules.maxWindSpeed,
|
||||
value: windSpeed,
|
||||
threshold: rules.maxWindSpeed,
|
||||
message: windSpeed > rules.maxWindSpeed ?
|
||||
`Wind speed too high (${windSpeed} mph > ${rules.maxWindSpeed} mph)` : 'Wind speed acceptable'
|
||||
},
|
||||
temperature: {
|
||||
suitable: temperature >= rules.minTemperature && temperature <= rules.maxTemperature,
|
||||
value: temperature,
|
||||
range: [rules.minTemperature, rules.maxTemperature],
|
||||
message: temperature < rules.minTemperature ?
|
||||
`Temperature too low (${Math.round(temperature)}°F < ${rules.minTemperature}°F)` :
|
||||
temperature > rules.maxTemperature ?
|
||||
`Temperature too high (${Math.round(temperature)}°F > ${rules.maxTemperature}°F)` :
|
||||
'Temperature acceptable'
|
||||
},
|
||||
humidity: {
|
||||
suitable: humidity <= rules.maxHumidity,
|
||||
value: humidity,
|
||||
threshold: rules.maxHumidity,
|
||||
message: humidity > rules.maxHumidity ?
|
||||
`Humidity too high (${humidity}% > ${rules.maxHumidity}%)` : 'Humidity acceptable'
|
||||
},
|
||||
precipitation: {
|
||||
suitable: precipitation <= rules.maxPrecipitation,
|
||||
value: precipitation,
|
||||
threshold: rules.maxPrecipitation,
|
||||
message: precipitation > rules.maxPrecipitation ?
|
||||
`Active precipitation (${precipitation} in/hr)` : 'No precipitation'
|
||||
}
|
||||
};
|
||||
|
||||
const overallSuitable = Object.values(checks).every(check => check.suitable);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
suitability: {
|
||||
overall: overallSuitable,
|
||||
applicationType: application_type,
|
||||
checks,
|
||||
recommendations: overallSuitable ?
|
||||
['Conditions are suitable for application'] :
|
||||
Object.values(checks)
|
||||
.filter(check => !check.suitable)
|
||||
.map(check => check.message),
|
||||
lastUpdated: new Date()
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
|
||||
throw new AppError('Weather service temporarily unavailable', 503);
|
||||
}
|
||||
if (error.response?.status === 401) {
|
||||
throw new AppError('Weather service authentication failed', 503);
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user