cleanup
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@react-pdf/renderer": "^4.3.0",
|
||||
"framer-motion": "^12.12.1",
|
||||
"lucide-react": "^0.515.0",
|
||||
"mongodb": "^6.3.0",
|
||||
"mongoose": "^8.15.0",
|
||||
"next": "14.1.0",
|
||||
@@ -1754,6 +1755,15 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.515.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.515.0.tgz",
|
||||
"integrity": "sha512-Sy7bY0MeicRm2pzrnoHm2h6C1iVoeHyBU2fjdQDsXGP51fhkhau1/ZV/dzrcxEmAKsxYb6bGaIsMnGHuQ5s0dw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/media-engine": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@react-pdf/renderer": "^4.3.0",
|
||||
"framer-motion": "^12.12.1",
|
||||
"lucide-react": "^0.515.0",
|
||||
"mongodb": "^6.3.0",
|
||||
"mongoose": "^8.15.0",
|
||||
"next": "14.1.0",
|
||||
|
||||
@@ -63,7 +63,7 @@ function calculateStatistics(data) {
|
||||
}
|
||||
} else {
|
||||
if (isCharging && chargingStart) {
|
||||
stats.battery.chargingTime += (timestamp - chargingStart) / (1000 * 60) // minutes
|
||||
stats.battery.chargingTime += (timestamp - chargingStart) / (1000 * 60)
|
||||
isCharging = false
|
||||
chargingStart = null
|
||||
}
|
||||
@@ -360,7 +360,6 @@ async function generatePDF(stats, timeRange, data) {
|
||||
height: 300
|
||||
})
|
||||
|
||||
// Draw power statistics
|
||||
const powerStats = [
|
||||
`Average PV Input Power: ${stats.power.avgPvInput.toFixed(1)}W`,
|
||||
`Maximum PV Input Power: ${stats.power.maxPvInput.toFixed(1)}W`,
|
||||
@@ -390,10 +389,8 @@ async function generatePDF(stats, timeRange, data) {
|
||||
|
||||
drawFooter(page2)
|
||||
|
||||
// Page 3: Temperature and System Health
|
||||
drawHeader(page3, 'System Health')
|
||||
|
||||
// Draw temperature chart
|
||||
const temperaturePdfImage = await pdfDoc.embedPng(charts.temperatureImage)
|
||||
page3.drawImage(temperaturePdfImage, {
|
||||
x: margin,
|
||||
@@ -402,7 +399,6 @@ async function generatePDF(stats, timeRange, data) {
|
||||
height: 300
|
||||
})
|
||||
|
||||
// Draw temperature statistics
|
||||
const tempStats = [
|
||||
`Average Inverter Temperature: ${stats.temperature.avgTemp.toFixed(1)}°C`,
|
||||
`Maximum Inverter Temperature: ${stats.temperature.maxTemp.toFixed(1)}°C`,
|
||||
@@ -427,7 +423,6 @@ async function generatePDF(stats, timeRange, data) {
|
||||
})
|
||||
})
|
||||
|
||||
// Draw system health summary
|
||||
const healthSummary = [
|
||||
'System Health Summary:',
|
||||
`• Battery Health: ${stats.battery.avgCapacity > 80 ? 'Excellent' : stats.battery.avgCapacity > 60 ? 'Good' : 'Needs Attention'}`,
|
||||
@@ -455,7 +450,6 @@ async function generatePDF(stats, timeRange, data) {
|
||||
|
||||
drawFooter(page3)
|
||||
|
||||
// Save the PDF
|
||||
return await pdfDoc.save()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,183 +1,256 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { connectToDatabase } from '@/lib/mongodb';
|
||||
|
||||
const CACHE_DURATION = 2 * 60 * 60 * 1000; // 2 hours in milliseconds
|
||||
|
||||
// Solar panel efficiency factors based on real-world data
|
||||
const WEATHER_IMPACT = {
|
||||
// Cloud coverage impact on solar efficiency (based on NREL data)
|
||||
CLOUD_IMPACT: {
|
||||
CLEAR: 1.0, // 0-10% clouds
|
||||
PARTLY_CLOUDY: 0.7, // 11-50% clouds
|
||||
MOSTLY_CLOUDY: 0.4, // 51-90% clouds
|
||||
OVERCAST: 0.2, // 91-100% clouds
|
||||
CLEAR: 1.0,
|
||||
PARTLY_CLOUDY: 0.75,
|
||||
MOSTLY_CLOUDY: 0.5,
|
||||
OVERCAST: 0.25,
|
||||
},
|
||||
// Temperature impact (based on solar panel temperature coefficient)
|
||||
TEMP_IMPACT: {
|
||||
OPTIMAL: 1.0, // 25°C (optimal temperature)
|
||||
COLD: 0.95, // < 10°C
|
||||
HOT: 0.85, // > 35°C
|
||||
OPTIMAL: 1.0,
|
||||
COLD: 0.9,
|
||||
HOT: 0.8,
|
||||
},
|
||||
// Rain impact
|
||||
RAIN_IMPACT: {
|
||||
NONE: 1.0, // 0% chance
|
||||
LIGHT: 0.8, // 1-30% chance
|
||||
MODERATE: 0.6, // 31-60% chance
|
||||
HEAVY: 0.4, // 61-100% chance
|
||||
NONE: 1.0,
|
||||
LIGHT: 0.7,
|
||||
HEAVY: 0.3,
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchWeatherData() {
|
||||
const response = await fetch(
|
||||
`http://api.weatherapi.com/v1/forecast.json?key=${process.env.WEATHER_API_KEY}&q=47.06149235737582,28.86683069635403&days=7&aqi=no`
|
||||
);
|
||||
if (!response.ok) throw new Error('Failed to fetch weather data');
|
||||
return response.json();
|
||||
}
|
||||
function getCloudImpact(description) {
|
||||
if (!description) return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY;
|
||||
|
||||
function getCloudImpact(cloudCover) {
|
||||
if (cloudCover <= 10) return WEATHER_IMPACT.CLOUD_IMPACT.CLEAR;
|
||||
if (cloudCover <= 50) return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY;
|
||||
if (cloudCover <= 90) return WEATHER_IMPACT.CLOUD_IMPACT.MOSTLY_CLOUDY;
|
||||
return WEATHER_IMPACT.CLOUD_IMPACT.OVERCAST;
|
||||
const desc = description.toLowerCase();
|
||||
if (desc.includes('clear') || desc.includes('sunny')) return WEATHER_IMPACT.CLOUD_IMPACT.CLEAR;
|
||||
if (desc.includes('few clouds') || desc.includes('partly')) return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY;
|
||||
if (desc.includes('scattered') || desc.includes('mostly')) return WEATHER_IMPACT.CLOUD_IMPACT.MOSTLY_CLOUDY;
|
||||
if (desc.includes('overcast') || desc.includes('cloudy')) return WEATHER_IMPACT.CLOUD_IMPACT.OVERCAST;
|
||||
return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY;
|
||||
}
|
||||
|
||||
function getTempImpact(temp) {
|
||||
if (temp >= 10 && temp <= 35) {
|
||||
// Linear interpolation for temperatures between optimal ranges
|
||||
if (temp <= 25) {
|
||||
return 1.0 - ((25 - temp) * 0.003); // 0.3% loss per degree below optimal
|
||||
} else {
|
||||
return 1.0 - ((temp - 25) * 0.01); // 1% loss per degree above optimal
|
||||
}
|
||||
if (typeof temp !== 'number' || isNaN(temp)) return WEATHER_IMPACT.TEMP_IMPACT.OPTIMAL;
|
||||
|
||||
if (temp >= 20 && temp <= 25) return WEATHER_IMPACT.TEMP_IMPACT.OPTIMAL;
|
||||
if (temp < 10) return WEATHER_IMPACT.TEMP_IMPACT.COLD;
|
||||
if (temp > 35) return WEATHER_IMPACT.TEMP_IMPACT.HOT;
|
||||
|
||||
if (temp < 20) {
|
||||
return 0.9 + ((temp - 10) / 10) * 0.1;
|
||||
} else {
|
||||
return 1.0 - ((temp - 25) / 10) * 0.2;
|
||||
}
|
||||
return temp < 10 ? WEATHER_IMPACT.TEMP_IMPACT.COLD : WEATHER_IMPACT.TEMP_IMPACT.HOT;
|
||||
}
|
||||
|
||||
function getRainImpact(rainChance) {
|
||||
if (rainChance <= 0) return WEATHER_IMPACT.RAIN_IMPACT.NONE;
|
||||
if (rainChance <= 30) return WEATHER_IMPACT.RAIN_IMPACT.LIGHT;
|
||||
if (rainChance <= 60) return WEATHER_IMPACT.RAIN_IMPACT.MODERATE;
|
||||
return WEATHER_IMPACT.RAIN_IMPACT.HEAVY;
|
||||
function getRainImpact(description) {
|
||||
if (!description) return WEATHER_IMPACT.RAIN_IMPACT.NONE;
|
||||
|
||||
const desc = description.toLowerCase();
|
||||
if (desc.includes('rain') || desc.includes('shower')) {
|
||||
if (desc.includes('heavy') || desc.includes('thunderstorm')) {
|
||||
return WEATHER_IMPACT.RAIN_IMPACT.HEAVY;
|
||||
}
|
||||
return WEATHER_IMPACT.RAIN_IMPACT.LIGHT;
|
||||
}
|
||||
if (desc.includes('snow') || desc.includes('thunderstorm')) {
|
||||
return WEATHER_IMPACT.RAIN_IMPACT.HEAVY;
|
||||
}
|
||||
return WEATHER_IMPACT.RAIN_IMPACT.NONE;
|
||||
}
|
||||
|
||||
async function calculatePrediction(weatherData, historicalData) {
|
||||
// Validate historical data
|
||||
if (!historicalData || historicalData.length === 0) {
|
||||
throw new Error('No historical data available for predictions');
|
||||
async function ensureWeatherData(db) {
|
||||
console.log('Checking for weather data...');
|
||||
|
||||
let weatherData = await db.collection('weather_data')
|
||||
.findOne({}, { sort: { timestamp: -1 } });
|
||||
|
||||
console.log('Weather data found:', !!weatherData);
|
||||
|
||||
if (!weatherData || !weatherData.forecasts) {
|
||||
console.log('No weather data found, attempting to fetch...');
|
||||
|
||||
try {
|
||||
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
|
||||
const response = await fetch(`${baseUrl}/api/weather?refresh=true`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Weather API call successful, checking database again...');
|
||||
weatherData = await db.collection('weather_data')
|
||||
.findOne({}, { sort: { timestamp: -1 } });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch weather data internally:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out invalid data points and calculate average
|
||||
const validHistoricalData = historicalData.filter(data =>
|
||||
data &&
|
||||
data.data &&
|
||||
typeof data.data.pv_input_power === 'number' &&
|
||||
!isNaN(data.data.pv_input_power)
|
||||
);
|
||||
return weatherData;
|
||||
}
|
||||
|
||||
if (validHistoricalData.length === 0) {
|
||||
throw new Error('No valid historical power data available');
|
||||
async function calculateSimplePrediction(weatherData, historicalData) {
|
||||
console.log('Calculating predictions with weather data:', weatherData?.length, 'historical data:', historicalData?.length);
|
||||
|
||||
let basePower = 800;
|
||||
|
||||
if (historicalData && historicalData.length > 0) {
|
||||
const validData = historicalData.filter(d =>
|
||||
d && d.data && typeof d.data.pv_input_power === 'number' && d.data.pv_input_power > 0
|
||||
);
|
||||
|
||||
if (validData.length > 0) {
|
||||
const avgPower = validData.reduce((sum, d) => sum + d.data.pv_input_power, 0) / validData.length;
|
||||
basePower = Math.max(200, avgPower);
|
||||
console.log('Calculated base power from historical data:', basePower);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate base power from historical data
|
||||
const historicalAvg = validHistoricalData.reduce((sum, data) => sum + data.data.pv_input_power, 0) / validHistoricalData.length;
|
||||
const basePower = Math.max(200, historicalAvg); // Minimum 200W baseline
|
||||
if (!weatherData || !Array.isArray(weatherData)) {
|
||||
console.error('Invalid weather data:', weatherData);
|
||||
throw new Error('Invalid weather data provided');
|
||||
}
|
||||
|
||||
// Calculate predictions for each day
|
||||
const predictions = weatherData.forecast.forecastday.map(day => {
|
||||
// Ensure we have the required data
|
||||
if (!day || !day.day) {
|
||||
throw new Error('Invalid weather data structure');
|
||||
const predictions = weatherData.slice(0, 7).map((dayWeather, index) => {
|
||||
if (!dayWeather) {
|
||||
throw new Error(`Weather data missing for day ${index}`);
|
||||
}
|
||||
|
||||
const cloudCover = day.day.cloud || 0;
|
||||
const rainChance = day.day.daily_chance_of_rain || 0;
|
||||
const avgTemp = ((day.day.maxtemp_c || 0) + (day.day.mintemp_c || 0)) / 2;
|
||||
|
||||
// Validate weather data
|
||||
if (typeof cloudCover !== 'number' || typeof rainChance !== 'number' || typeof avgTemp !== 'number') {
|
||||
console.error('Weather data validation failed:', { cloudCover, rainChance, avgTemp });
|
||||
throw new Error('Invalid weather data received');
|
||||
}
|
||||
|
||||
// Calculate weather impact factors
|
||||
const cloudImpact = getCloudImpact(cloudCover);
|
||||
const avgTemp = dayWeather.temp_max && dayWeather.temp_min
|
||||
? (dayWeather.temp_max + dayWeather.temp_min) / 2
|
||||
: 20; // Default temp
|
||||
|
||||
const cloudImpact = getCloudImpact(dayWeather.description);
|
||||
const tempImpact = getTempImpact(avgTemp);
|
||||
const rainImpact = getRainImpact(rainChance);
|
||||
|
||||
// Calculate final prediction with all factors
|
||||
const rainImpact = getRainImpact(dayWeather.description);
|
||||
|
||||
const weatherFactor = cloudImpact * tempImpact * rainImpact;
|
||||
const predictedPower = Math.round(basePower * weatherFactor);
|
||||
|
||||
// Calculate confidence based on weather stability
|
||||
const weatherStability = 1 - (Math.abs(cloudImpact - 1) + Math.abs(tempImpact - 1) + Math.abs(rainImpact - 1)) / 3;
|
||||
const confidence = 0.7 + (weatherStability * 0.3);
|
||||
|
||||
|
||||
const daysFuture = index + 1;
|
||||
const baseConfidence = 0.9 - (daysFuture * 0.1);
|
||||
const weatherConfidence = (cloudImpact + tempImpact + rainImpact) / 3;
|
||||
const confidence = Math.max(0.3, baseConfidence * weatherConfidence);
|
||||
|
||||
return {
|
||||
date: new Date(day.date),
|
||||
date: dayWeather.date ? new Date(dayWeather.date) : new Date(Date.now() + index * 24 * 60 * 60 * 1000),
|
||||
predictedPower,
|
||||
weatherData: day,
|
||||
weatherData: dayWeather,
|
||||
confidence: Math.round(confidence * 100) / 100,
|
||||
factors: {
|
||||
cloudImpact,
|
||||
tempImpact,
|
||||
rainImpact
|
||||
rainImpact,
|
||||
weatherFactor
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
console.log('Generated predictions:', predictions.length);
|
||||
return predictions;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request) {
|
||||
console.log('GET /api/predictions called');
|
||||
|
||||
try {
|
||||
const { db } = await connectToDatabase();
|
||||
const now = new Date();
|
||||
|
||||
// Check for cached predictions
|
||||
const cachedPredictions = await db.collection('predictions')
|
||||
.find({
|
||||
date: { $gte: now },
|
||||
lastUpdated: { $gte: new Date(now.getTime() - CACHE_DURATION) }
|
||||
})
|
||||
.sort({ date: 1 })
|
||||
.toArray();
|
||||
|
||||
if (cachedPredictions.length === 7) {
|
||||
return NextResponse.json(cachedPredictions);
|
||||
}
|
||||
|
||||
// Fetch fresh weather data
|
||||
const weatherData = await fetchWeatherData();
|
||||
|
||||
// Get historical power data for the last 30 days
|
||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
const historicalData = await db.collection('solar_data')
|
||||
.find({ timestamp: { $gte: thirtyDaysAgo } })
|
||||
.toArray();
|
||||
|
||||
// Calculate new predictions
|
||||
const predictions = await calculatePrediction(weatherData, historicalData);
|
||||
|
||||
// Store predictions in database
|
||||
const bulkOps = predictions.map(prediction => ({
|
||||
updateOne: {
|
||||
filter: { date: prediction.date },
|
||||
update: { $set: prediction },
|
||||
upsert: true
|
||||
console.log('Database connected successfully');
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const forceRefresh = searchParams.get('refresh') === 'true';
|
||||
console.log('Force refresh:', forceRefresh);
|
||||
|
||||
if (!forceRefresh) {
|
||||
console.log('Checking for cached predictions...');
|
||||
const existingPredictions = await db.collection('predictions')
|
||||
.find({})
|
||||
.sort({ date: 1 })
|
||||
.toArray();
|
||||
|
||||
console.log('Found cached predictions:', existingPredictions.length);
|
||||
|
||||
if (existingPredictions.length > 0) {
|
||||
const sixHoursAgo = new Date(Date.now() - 6 * 60 * 60 * 1000);
|
||||
const latestPrediction = existingPredictions[0];
|
||||
|
||||
if (latestPrediction.lastUpdated && new Date(latestPrediction.lastUpdated) > sixHoursAgo) {
|
||||
return NextResponse.json({
|
||||
predictions: existingPredictions,
|
||||
fromCache: true,
|
||||
lastUpdated: latestPrediction.lastUpdated
|
||||
});
|
||||
} else {
|
||||
console.log('Cached predictions are stale, will refresh');
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
if (bulkOps.length > 0) {
|
||||
await db.collection('predictions').bulkWrite(bulkOps);
|
||||
}
|
||||
|
||||
return NextResponse.json(predictions);
|
||||
|
||||
const weatherData = await ensureWeatherData(db);
|
||||
|
||||
if (!weatherData || !weatherData.forecasts) {
|
||||
console.error('No weather data available after attempting to fetch');
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'No weather data available. Please check your weather API configuration.',
|
||||
suggestion: 'Ensure WEATHER_API_KEY is set and the weather API is accessible.'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Weather data available with forecasts:', weatherData.forecasts.length);
|
||||
|
||||
console.log('Fetching historical solar data...');
|
||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
const historicalData = await db.collection('solar_data')
|
||||
.find({ timestamp: { $gte: thirtyDaysAgo } })
|
||||
.toArray();
|
||||
|
||||
console.log('Historical data found:', historicalData.length, 'records');
|
||||
|
||||
console.log('Calculating new predictions...');
|
||||
const predictions = await calculateSimplePrediction(weatherData.forecasts, historicalData);
|
||||
|
||||
const now = new Date();
|
||||
const predictionsWithTimestamp = predictions.map(pred => ({
|
||||
...pred,
|
||||
lastUpdated: now
|
||||
}));
|
||||
|
||||
console.log('Storing predictions in database...');
|
||||
await db.collection('predictions').deleteMany({});
|
||||
if (predictionsWithTimestamp.length > 0) {
|
||||
await db.collection('predictions').insertMany(predictionsWithTimestamp);
|
||||
}
|
||||
|
||||
console.log('Predictions stored successfully');
|
||||
|
||||
return NextResponse.json({
|
||||
predictions: predictionsWithTimestamp,
|
||||
fromCache: false,
|
||||
lastUpdated: now
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Prediction error:', error);
|
||||
console.error('Prediction API error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to generate predictions' },
|
||||
{ status: 500 }
|
||||
{
|
||||
error: error.message || 'Failed to generate predictions',
|
||||
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request) {
|
||||
console.log('POST /api/predictions called - forcing refresh');
|
||||
return GET(new Request(request.url + '?refresh=true'));
|
||||
}
|
||||
@@ -14,7 +14,6 @@ export async function GET(request) {
|
||||
let startDate = new Date()
|
||||
let endDate = new Date()
|
||||
|
||||
// If since parameter is provided, use it as the start date
|
||||
if (since) {
|
||||
startDate = new Date(since)
|
||||
} else {
|
||||
|
||||
@@ -8,67 +8,145 @@ const COORDS = {
|
||||
}
|
||||
|
||||
async function fetchWeatherFromAPI() {
|
||||
const response = await fetch(
|
||||
`http://api.weatherapi.com/v1/forecast.json?key=${process.env.WEATHER_API_KEY}&q=${COORDS.lat},${COORDS.lon}&days=7&aqi=no`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch weather data')
|
||||
console.log('Fetching weather from WeatherAPI...')
|
||||
|
||||
if (!process.env.WEATHER_API_KEY) {
|
||||
throw new Error('WEATHER_API_KEY environment variable is not set')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
|
||||
const response = await fetch(
|
||||
`http://api.weatherapi.com/v1/forecast.json?key=${process.env.WEATHER_API_KEY}&q=${COORDS.lat},${COORDS.lon}&days=7&aqi=no`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'SolarPrediction/1.0'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('WeatherAPI Error:', response.status, errorText)
|
||||
throw new Error(`Weather API failed: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('Weather data fetched successfully:', !!data.forecast?.forecastday)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Ensure database connection
|
||||
const { db } = await connectToDatabase()
|
||||
function convertToInternalFormat(weatherApiData) {
|
||||
if (!weatherApiData.forecast?.forecastday) {
|
||||
throw new Error('Invalid weather data structure from WeatherAPI')
|
||||
}
|
||||
|
||||
return weatherApiData.forecast.forecastday.map(day => ({
|
||||
date: day.date,
|
||||
temp_min: day.day.mintemp_c,
|
||||
temp_max: day.day.maxtemp_c,
|
||||
humidity: day.day.avghumidity,
|
||||
wind_speed: day.day.maxwind_kph / 3.6,
|
||||
icon: day.day.condition.icon,
|
||||
description: day.day.condition.text.toLowerCase(),
|
||||
timestamp: new Date(day.date)
|
||||
}))
|
||||
}
|
||||
|
||||
export async function GET(request) {
|
||||
console.log('Weather API GET called')
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const forceRefresh = searchParams.get('refresh') === 'true'
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
console.log('Database connected successfully')
|
||||
|
||||
// Check if we have cached data
|
||||
const cachedWeather = await db.collection('weather').findOne({
|
||||
'location.lat': COORDS.lat,
|
||||
'location.lon': COORDS.lon
|
||||
})
|
||||
|
||||
const now = new Date()
|
||||
const isCacheValid = cachedWeather &&
|
||||
(now - new Date(cachedWeather.lastUpdated)) < CACHE_DURATION
|
||||
const isCacheValid = cachedWeather &&
|
||||
(now - new Date(cachedWeather.lastUpdated)) < CACHE_DURATION
|
||||
|
||||
// If cache is valid, return cached data
|
||||
if (isCacheValid) {
|
||||
console.log('Cache status:', {
|
||||
hasCached: !!cachedWeather,
|
||||
isCacheValid,
|
||||
forceRefresh
|
||||
})
|
||||
|
||||
if (isCacheValid && !forceRefresh) {
|
||||
console.log('Returning cached weather data')
|
||||
return NextResponse.json(cachedWeather)
|
||||
}
|
||||
|
||||
// If no cache or cache is invalid, fetch fresh data
|
||||
console.log('Fetching fresh weather data...')
|
||||
const weatherData = await fetchWeatherFromAPI()
|
||||
|
||||
// Add metadata to the weather data
|
||||
|
||||
const weatherWithMetadata = {
|
||||
...weatherData,
|
||||
location: COORDS,
|
||||
lastUpdated: now
|
||||
}
|
||||
|
||||
// Update or create weather document
|
||||
await db.collection('weather').updateOne(
|
||||
{
|
||||
'location.lat': COORDS.lat,
|
||||
'location.lon': COORDS.lon
|
||||
},
|
||||
{ $set: weatherWithMetadata },
|
||||
{ upsert: true }
|
||||
{
|
||||
'location.lat': COORDS.lat,
|
||||
'location.lon': COORDS.lon
|
||||
},
|
||||
{ $set: weatherWithMetadata },
|
||||
{ upsert: true }
|
||||
)
|
||||
|
||||
const internalFormatData = convertToInternalFormat(weatherData)
|
||||
|
||||
await db.collection('weather_data').updateOne(
|
||||
{ type: 'forecast' },
|
||||
{
|
||||
$set: {
|
||||
forecasts: internalFormatData,
|
||||
timestamp: now,
|
||||
source: 'weatherapi'
|
||||
}
|
||||
},
|
||||
{ upsert: true }
|
||||
)
|
||||
|
||||
console.log('Weather data stored in both formats successfully')
|
||||
|
||||
return NextResponse.json(weatherWithMetadata)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Weather API Error:', error)
|
||||
// If we have cached data but failed to fetch new data, return cached data
|
||||
if (cachedWeather) {
|
||||
return NextResponse.json(cachedWeather)
|
||||
|
||||
try {
|
||||
const { db } = await connectToDatabase()
|
||||
const cachedWeather = await db.collection('weather').findOne({
|
||||
'location.lat': COORDS.lat,
|
||||
'location.lon': COORDS.lon
|
||||
})
|
||||
|
||||
if (cachedWeather) {
|
||||
console.log('Returning stale cached data due to API error')
|
||||
return NextResponse.json({
|
||||
...cachedWeather,
|
||||
isStale: true,
|
||||
error: 'Using cached data due to API error'
|
||||
})
|
||||
}
|
||||
} catch (dbError) {
|
||||
console.error('Database error while fetching cached data:', dbError)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch weather data' },
|
||||
{ status: 500 }
|
||||
{
|
||||
error: 'Failed to fetch weather data',
|
||||
details: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,7 @@ import { BatteryStatus } from '@/components/BatteryStatus'
|
||||
import { PowerStats } from '@/components/PowerStats'
|
||||
import { SystemStatus } from '@/components/SystemStatus'
|
||||
import { SolarChart } from '@/components/SolarChart'
|
||||
import { LastRefresh } from '@/components/LastRefresh'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Navbar } from '@/components/Navbar'
|
||||
import WeatherForecast from '@/components/WeatherForecast'
|
||||
import PowerPredictionGraph from '@/components/PowerPredictionGraph'
|
||||
@@ -32,7 +30,6 @@ export default function Dashboard() {
|
||||
|
||||
if (newData.length > 0) {
|
||||
setCurrentData(prevData => {
|
||||
// Merge new data with existing data, avoiding duplicates
|
||||
const mergedData = [...prevData]
|
||||
newData.forEach(newItem => {
|
||||
const newTimestamp = newItem.timestamp.$date || newItem.timestamp
|
||||
@@ -48,11 +45,9 @@ export default function Dashboard() {
|
||||
return mergedData
|
||||
})
|
||||
|
||||
// Update last update time
|
||||
const latestTimestamp = newData[newData.length - 1].timestamp.$date || newData[newData.length - 1].timestamp
|
||||
setLastUpdateTime(latestTimestamp)
|
||||
|
||||
// Update chart data if viewing today's data
|
||||
if (chartTimeRange === 'today') {
|
||||
setData(prevData => {
|
||||
const mergedData = [...prevData]
|
||||
@@ -78,7 +73,7 @@ export default function Dashboard() {
|
||||
|
||||
const fetchHistoricalData = async () => {
|
||||
try {
|
||||
setLastUpdateTime(null) // Reset last update time when changing time range
|
||||
setLastUpdateTime(null);
|
||||
const response = await fetch(`/api/solar-data?timeRange=${chartTimeRange}`)
|
||||
const newData = await response.json()
|
||||
setData(newData)
|
||||
@@ -90,7 +85,6 @@ export default function Dashboard() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Set minimum loading time
|
||||
const minLoadingTimer = setTimeout(() => {
|
||||
setMinLoadingComplete(true)
|
||||
}, 500)
|
||||
|
||||
@@ -13,121 +13,310 @@ import {
|
||||
Legend
|
||||
} from 'recharts';
|
||||
import { motion } from 'framer-motion';
|
||||
import { RefreshCw, Clock, Database, AlertCircle } from 'lucide-react';
|
||||
|
||||
export default function PowerPredictionGraph() {
|
||||
const [predictions, setPredictions] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [lastUpdated, setLastUpdated] = useState(null);
|
||||
const [fromCache, setFromCache] = useState(false);
|
||||
const [debugInfo, setDebugInfo] = useState('');
|
||||
|
||||
const fetchPredictions = async (forceRefresh = false) => {
|
||||
const debugLog = [];
|
||||
|
||||
try {
|
||||
debugLog.push(`Starting fetch - forceRefresh: ${forceRefresh}`);
|
||||
|
||||
if (forceRefresh) {
|
||||
setRefreshing(true);
|
||||
debugLog.push('Setting refreshing state to true');
|
||||
} else {
|
||||
setLoading(true);
|
||||
debugLog.push('Setting loading state to true');
|
||||
}
|
||||
|
||||
const url = forceRefresh ? '/api/predictions?refresh=true' : '/api/predictions';
|
||||
debugLog.push(`Fetching from URL: ${url}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
debugLog.push(`Response status: ${response.status}`);
|
||||
debugLog.push(`Response ok: ${response.ok}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
debugLog.push(`Error response text: ${errorText}`);
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
debugLog.push(`Response data received - predictions: ${data.predictions?.length}, fromCache: ${data.fromCache}`);
|
||||
|
||||
if (!data.predictions || !Array.isArray(data.predictions)) {
|
||||
debugLog.push('Invalid data structure received');
|
||||
throw new Error('Invalid response data structure');
|
||||
}
|
||||
|
||||
setPredictions(data.predictions);
|
||||
setLastUpdated(new Date(data.lastUpdated));
|
||||
setFromCache(data.fromCache);
|
||||
setError(null);
|
||||
debugLog.push('State updated successfully');
|
||||
|
||||
} catch (err) {
|
||||
debugLog.push(`Error caught: ${err.message}`);
|
||||
console.error('Fetch error:', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
setDebugInfo(debugLog.join('\n'));
|
||||
debugLog.push('Loading states reset');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPredictions = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/predictions');
|
||||
if (!response.ok) throw new Error('Failed to fetch predictions');
|
||||
const data = await response.json();
|
||||
setPredictions(data);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Component mounted, fetching initial predictions');
|
||||
fetchPredictions();
|
||||
}, []);
|
||||
|
||||
const handleRefresh = () => {
|
||||
console.log('Refresh button clicked');
|
||||
fetchPredictions(true);
|
||||
};
|
||||
|
||||
const formatLastUpdated = (date) => {
|
||||
if (!date) return 'Unknown';
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'Just now';
|
||||
if (diffMins < 60) return `${diffMins} min ago`;
|
||||
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||||
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="relative"
|
||||
>
|
||||
<div className="w-16 h-16 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-12 h-12 border-4 border-emerald-500 border-t-transparent rounded-full animate-spin" style={{ animationDirection: 'reverse' }}></div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="relative"
|
||||
>
|
||||
<div className="w-16 h-16 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-12 h-12 border-4 border-emerald-500 border-t-transparent rounded-full animate-spin" style={{ animationDirection: 'reverse' }}></div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-red-500">Error loading predictions</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700">
|
||||
<div className="flex flex-col items-center justify-center h-64 space-y-4">
|
||||
<div className="text-red-500 text-center">
|
||||
<AlertCircle className="w-12 h-12 mx-auto mb-2" />
|
||||
<p className="font-semibold">Error loading predictions</p>
|
||||
<p className="text-sm text-gray-400 mt-1">{error}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 text-white rounded-lg transition-colors duration-200"
|
||||
>
|
||||
{refreshing ? 'Retrying...' : 'Try Again'}
|
||||
</button>
|
||||
|
||||
{/* Debug information */}
|
||||
{process.env.NODE_ENV === 'development' && debugInfo && (
|
||||
<details className="mt-4 w-full">
|
||||
<summary className="text-xs text-gray-500 cursor-pointer">Debug Info</summary>
|
||||
<pre className="text-xs text-gray-400 mt-2 bg-gray-900 p-2 rounded overflow-auto max-h-32">
|
||||
{debugInfo}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!predictions?.length) {
|
||||
return null;
|
||||
return (
|
||||
<Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700">
|
||||
<div className="flex flex-col items-center justify-center h-64 space-y-4">
|
||||
<div className="text-gray-400 text-center">
|
||||
<Database className="w-12 h-12 mx-auto mb-2" />
|
||||
<p>No prediction data available</p>
|
||||
<p className="text-sm">Click generate to create predictions</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 text-white rounded-lg transition-colors duration-200"
|
||||
>
|
||||
{refreshing ? 'Generating...' : 'Generate Predictions'}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const chartData = predictions.map(pred => ({
|
||||
date: new Date(pred.date).toLocaleDateString('en-US', { weekday: 'short' }),
|
||||
date: new Date(pred.date).toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}),
|
||||
predicted: pred.predictedPower,
|
||||
confidence: pred.confidence * 100
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700 hover:bg-gray-800/70 transition-all duration-300">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">7-Day Power Generation Forecast</h2>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke="#9CA3AF"
|
||||
tick={{ fill: '#9CA3AF' }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#9CA3AF"
|
||||
tick={{ fill: '#9CA3AF' }}
|
||||
label={{
|
||||
value: 'Power (W)',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
fill: '#9CA3AF'
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1F2937',
|
||||
border: '1px solid #374151',
|
||||
borderRadius: '0.375rem',
|
||||
color: '#F3F4F6'
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="predicted"
|
||||
name="Predicted Power"
|
||||
stroke="#60A5FA"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#60A5FA', strokeWidth: 2 }}
|
||||
activeDot={{ r: 8 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="confidence"
|
||||
name="Confidence %"
|
||||
stroke="#34D399"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#34D399', strokeWidth: 2 }}
|
||||
activeDot={{ r: 8 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700 hover:bg-gray-800/70 transition-all duration-300">
|
||||
{/* Header with title and refresh button */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-white">7-Day Power Generation Forecast</h2>
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Cache indicator */}
|
||||
<div className="flex items-center space-x-1 text-xs text-gray-400">
|
||||
{fromCache ? (
|
||||
<>
|
||||
<Database className="w-3 h-3" />
|
||||
<span>Cached</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
<span>Fresh</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Last updated */}
|
||||
<div className="flex items-center space-x-1 text-xs text-gray-400">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formatLastUpdated(lastUpdated)}</span>
|
||||
</div>
|
||||
|
||||
{/* Refresh button */}
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="flex items-center space-x-1 px-3 py-1 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 text-white text-sm rounded-lg transition-colors duration-200"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
<span>{refreshing ? 'Updating...' : 'Refresh'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke="#9CA3AF"
|
||||
tick={{ fill: '#9CA3AF', fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#9CA3AF"
|
||||
tick={{ fill: '#9CA3AF', fontSize: 12 }}
|
||||
label={{
|
||||
value: 'Power (W)',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
fill: '#9CA3AF'
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1F2937',
|
||||
border: '1px solid #374151',
|
||||
borderRadius: '0.375rem',
|
||||
color: '#F3F4F6'
|
||||
}}
|
||||
formatter={(value, name) => [
|
||||
`${value}${name === 'Predicted Power' ? 'W' : '%'}`,
|
||||
name === 'Predicted Power' ? 'Predicted Power' : 'Confidence'
|
||||
]}
|
||||
/>
|
||||
<Legend />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="predicted"
|
||||
name="Predicted Power"
|
||||
stroke="#60A5FA"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#60A5FA', strokeWidth: 2 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="confidence"
|
||||
name="Confidence %"
|
||||
stroke="#34D399"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#34D399', strokeWidth: 2 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-gray-700/50 rounded-lg">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div className="text-center">
|
||||
<div className="text-gray-400">Avg. Predicted</div>
|
||||
<div className="text-white font-semibold">
|
||||
{Math.round(predictions.reduce((sum, p) => sum + p.predictedPower, 0) / predictions.length)}W
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-gray-400">Best Day</div>
|
||||
<div className="text-green-400 font-semibold">
|
||||
{Math.max(...predictions.map(p => p.predictedPower))}W
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-gray-400">Worst Day</div>
|
||||
<div className="text-red-400 font-semibold">
|
||||
{Math.min(...predictions.map(p => p.predictedPower))}W
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-gray-400">Avg. Confidence</div>
|
||||
<div className="text-blue-400 font-semibold">
|
||||
{Math.round(predictions.reduce((sum, p) => sum + p.confidence, 0) / predictions.length * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
Predictions: {predictions.length} | From Cache: {fromCache ? 'Yes' : 'No'}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -47,11 +47,9 @@ export function SolarChart({ data, type, timeRange, onTimeRangeChange }) {
|
||||
|
||||
const formatData = (data) => {
|
||||
return getFilteredData(data).map(item => {
|
||||
// Handle both MongoDB date format and regular date string
|
||||
const dateStr = item.timestamp.$date || item.timestamp
|
||||
const date = new Date(dateStr)
|
||||
|
||||
// Check if date is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
console.error('Invalid date:', dateStr)
|
||||
return null
|
||||
|
||||
@@ -8,23 +8,70 @@ const predictionSchema = new mongoose.Schema({
|
||||
},
|
||||
predictedPower: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
min: 0
|
||||
},
|
||||
weatherData: {
|
||||
type: Object,
|
||||
required: true
|
||||
date: String,
|
||||
temp_min: Number,
|
||||
temp_max: Number,
|
||||
humidity: Number,
|
||||
wind_speed: Number,
|
||||
icon: String,
|
||||
description: String,
|
||||
timestamp: Date
|
||||
},
|
||||
confidence: {
|
||||
type: Number,
|
||||
required: true
|
||||
required: true,
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
factors: {
|
||||
cloudImpact: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
tempImpact: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
rainImpact: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
weatherFactor: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
max: 1
|
||||
}
|
||||
},
|
||||
lastUpdated: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
default: Date.now,
|
||||
index: true
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Create compound index for efficient querying
|
||||
predictionSchema.index({ date: 1, lastUpdated: 1 });
|
||||
predictionSchema.index({ date: 1, lastUpdated: -1 });
|
||||
|
||||
export const Prediction = mongoose.models.Prediction || mongoose.model('Prediction', predictionSchema);
|
||||
predictionSchema.methods.isStale = function() {
|
||||
const sixHoursAgo = new Date(Date.now() - 6 * 60 * 60 * 1000);
|
||||
return this.lastUpdated < sixHoursAgo;
|
||||
};
|
||||
|
||||
predictionSchema.statics.getFreshPredictions = function() {
|
||||
const sixHoursAgo = new Date(Date.now() - 6 * 60 * 60 * 1000);
|
||||
return this.find({ lastUpdated: { $gte: sixHoursAgo } }).sort({ date: 1 });
|
||||
};
|
||||
|
||||
export const Prediction = mongoose.models.Prediction || mongoose.model('Prediction', predictionSchema);
|
||||
@@ -11,15 +11,12 @@ let client
|
||||
let clientPromise
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// In development mode, use a global variable so that the value
|
||||
// is preserved across module reloads caused by HMR (Hot Module Replacement).
|
||||
if (!global._mongoClientPromise) {
|
||||
client = new MongoClient(uri, options)
|
||||
global._mongoClientPromise = client.connect()
|
||||
}
|
||||
clientPromise = global._mongoClientPromise
|
||||
} else {
|
||||
// In production mode, it's best to not use a global variable.
|
||||
client = new MongoClient(uri, options)
|
||||
clientPromise = client.connect()
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ export async function fetchWeatherData() {
|
||||
)
|
||||
const data = await response.json()
|
||||
|
||||
// Process the data to get daily forecasts
|
||||
const dailyForecasts = data.list.reduce((acc, item) => {
|
||||
const date = new Date(item.dt * 1000)
|
||||
const day = date.toISOString().split('T')[0]
|
||||
@@ -67,10 +66,8 @@ export async function getWeatherData() {
|
||||
const { db } = await connectToDatabase()
|
||||
const collection = db.collection('weather_data')
|
||||
|
||||
// Get the latest weather data
|
||||
const latestData = await collection.findOne({}, { sort: { timestamp: -1 } })
|
||||
|
||||
// If no data exists or data is older than 2 hours, fetch new data
|
||||
if (!latestData || (Date.now() - latestData.timestamp.getTime() > 2 * 60 * 60 * 1000)) {
|
||||
const newData = await fetchWeatherData()
|
||||
await collection.insertOne({
|
||||
|
||||
Reference in New Issue
Block a user