cleanup
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.0",
|
"@react-pdf/renderer": "^4.3.0",
|
||||||
"framer-motion": "^12.12.1",
|
"framer-motion": "^12.12.1",
|
||||||
|
"lucide-react": "^0.515.0",
|
||||||
"mongodb": "^6.3.0",
|
"mongodb": "^6.3.0",
|
||||||
"mongoose": "^8.15.0",
|
"mongoose": "^8.15.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
@@ -1754,6 +1755,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/media-engine": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-pdf/renderer": "^4.3.0",
|
"@react-pdf/renderer": "^4.3.0",
|
||||||
"framer-motion": "^12.12.1",
|
"framer-motion": "^12.12.1",
|
||||||
|
"lucide-react": "^0.515.0",
|
||||||
"mongodb": "^6.3.0",
|
"mongodb": "^6.3.0",
|
||||||
"mongoose": "^8.15.0",
|
"mongoose": "^8.15.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function calculateStatistics(data) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isCharging && chargingStart) {
|
if (isCharging && chargingStart) {
|
||||||
stats.battery.chargingTime += (timestamp - chargingStart) / (1000 * 60) // minutes
|
stats.battery.chargingTime += (timestamp - chargingStart) / (1000 * 60)
|
||||||
isCharging = false
|
isCharging = false
|
||||||
chargingStart = null
|
chargingStart = null
|
||||||
}
|
}
|
||||||
@@ -360,7 +360,6 @@ async function generatePDF(stats, timeRange, data) {
|
|||||||
height: 300
|
height: 300
|
||||||
})
|
})
|
||||||
|
|
||||||
// Draw power statistics
|
|
||||||
const powerStats = [
|
const powerStats = [
|
||||||
`Average PV Input Power: ${stats.power.avgPvInput.toFixed(1)}W`,
|
`Average PV Input Power: ${stats.power.avgPvInput.toFixed(1)}W`,
|
||||||
`Maximum PV Input Power: ${stats.power.maxPvInput.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)
|
drawFooter(page2)
|
||||||
|
|
||||||
// Page 3: Temperature and System Health
|
|
||||||
drawHeader(page3, 'System Health')
|
drawHeader(page3, 'System Health')
|
||||||
|
|
||||||
// Draw temperature chart
|
|
||||||
const temperaturePdfImage = await pdfDoc.embedPng(charts.temperatureImage)
|
const temperaturePdfImage = await pdfDoc.embedPng(charts.temperatureImage)
|
||||||
page3.drawImage(temperaturePdfImage, {
|
page3.drawImage(temperaturePdfImage, {
|
||||||
x: margin,
|
x: margin,
|
||||||
@@ -402,7 +399,6 @@ async function generatePDF(stats, timeRange, data) {
|
|||||||
height: 300
|
height: 300
|
||||||
})
|
})
|
||||||
|
|
||||||
// Draw temperature statistics
|
|
||||||
const tempStats = [
|
const tempStats = [
|
||||||
`Average Inverter Temperature: ${stats.temperature.avgTemp.toFixed(1)}°C`,
|
`Average Inverter Temperature: ${stats.temperature.avgTemp.toFixed(1)}°C`,
|
||||||
`Maximum Inverter Temperature: ${stats.temperature.maxTemp.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 = [
|
const healthSummary = [
|
||||||
'System Health Summary:',
|
'System Health Summary:',
|
||||||
`• Battery Health: ${stats.battery.avgCapacity > 80 ? 'Excellent' : stats.battery.avgCapacity > 60 ? 'Good' : 'Needs Attention'}`,
|
`• 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)
|
drawFooter(page3)
|
||||||
|
|
||||||
// Save the PDF
|
|
||||||
return await pdfDoc.save()
|
return await pdfDoc.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,183 +1,256 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { connectToDatabase } from '@/lib/mongodb';
|
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 = {
|
const WEATHER_IMPACT = {
|
||||||
// Cloud coverage impact on solar efficiency (based on NREL data)
|
|
||||||
CLOUD_IMPACT: {
|
CLOUD_IMPACT: {
|
||||||
CLEAR: 1.0, // 0-10% clouds
|
CLEAR: 1.0,
|
||||||
PARTLY_CLOUDY: 0.7, // 11-50% clouds
|
PARTLY_CLOUDY: 0.75,
|
||||||
MOSTLY_CLOUDY: 0.4, // 51-90% clouds
|
MOSTLY_CLOUDY: 0.5,
|
||||||
OVERCAST: 0.2, // 91-100% clouds
|
OVERCAST: 0.25,
|
||||||
},
|
},
|
||||||
// Temperature impact (based on solar panel temperature coefficient)
|
|
||||||
TEMP_IMPACT: {
|
TEMP_IMPACT: {
|
||||||
OPTIMAL: 1.0, // 25°C (optimal temperature)
|
OPTIMAL: 1.0,
|
||||||
COLD: 0.95, // < 10°C
|
COLD: 0.9,
|
||||||
HOT: 0.85, // > 35°C
|
HOT: 0.8,
|
||||||
},
|
},
|
||||||
// Rain impact
|
|
||||||
RAIN_IMPACT: {
|
RAIN_IMPACT: {
|
||||||
NONE: 1.0, // 0% chance
|
NONE: 1.0,
|
||||||
LIGHT: 0.8, // 1-30% chance
|
LIGHT: 0.7,
|
||||||
MODERATE: 0.6, // 31-60% chance
|
HEAVY: 0.3,
|
||||||
HEAVY: 0.4, // 61-100% chance
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function fetchWeatherData() {
|
function getCloudImpact(description) {
|
||||||
const response = await fetch(
|
if (!description) return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY;
|
||||||
`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(cloudCover) {
|
const desc = description.toLowerCase();
|
||||||
if (cloudCover <= 10) return WEATHER_IMPACT.CLOUD_IMPACT.CLEAR;
|
if (desc.includes('clear') || desc.includes('sunny')) return WEATHER_IMPACT.CLOUD_IMPACT.CLEAR;
|
||||||
if (cloudCover <= 50) return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY;
|
if (desc.includes('few clouds') || desc.includes('partly')) return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY;
|
||||||
if (cloudCover <= 90) return WEATHER_IMPACT.CLOUD_IMPACT.MOSTLY_CLOUDY;
|
if (desc.includes('scattered') || desc.includes('mostly')) return WEATHER_IMPACT.CLOUD_IMPACT.MOSTLY_CLOUDY;
|
||||||
return WEATHER_IMPACT.CLOUD_IMPACT.OVERCAST;
|
if (desc.includes('overcast') || desc.includes('cloudy')) return WEATHER_IMPACT.CLOUD_IMPACT.OVERCAST;
|
||||||
|
return WEATHER_IMPACT.CLOUD_IMPACT.PARTLY_CLOUDY;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTempImpact(temp) {
|
function getTempImpact(temp) {
|
||||||
if (temp >= 10 && temp <= 35) {
|
if (typeof temp !== 'number' || isNaN(temp)) return WEATHER_IMPACT.TEMP_IMPACT.OPTIMAL;
|
||||||
// Linear interpolation for temperatures between optimal ranges
|
|
||||||
if (temp <= 25) {
|
if (temp >= 20 && temp <= 25) return WEATHER_IMPACT.TEMP_IMPACT.OPTIMAL;
|
||||||
return 1.0 - ((25 - temp) * 0.003); // 0.3% loss per degree below optimal
|
if (temp < 10) return WEATHER_IMPACT.TEMP_IMPACT.COLD;
|
||||||
} else {
|
if (temp > 35) return WEATHER_IMPACT.TEMP_IMPACT.HOT;
|
||||||
return 1.0 - ((temp - 25) * 0.01); // 1% loss per degree above optimal
|
|
||||||
}
|
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) {
|
function getRainImpact(description) {
|
||||||
if (rainChance <= 0) return WEATHER_IMPACT.RAIN_IMPACT.NONE;
|
if (!description) return WEATHER_IMPACT.RAIN_IMPACT.NONE;
|
||||||
if (rainChance <= 30) return WEATHER_IMPACT.RAIN_IMPACT.LIGHT;
|
|
||||||
if (rainChance <= 60) return WEATHER_IMPACT.RAIN_IMPACT.MODERATE;
|
const desc = description.toLowerCase();
|
||||||
return WEATHER_IMPACT.RAIN_IMPACT.HEAVY;
|
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) {
|
async function ensureWeatherData(db) {
|
||||||
// Validate historical data
|
console.log('Checking for weather data...');
|
||||||
if (!historicalData || historicalData.length === 0) {
|
|
||||||
throw new Error('No historical data available for predictions');
|
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
|
return weatherData;
|
||||||
const validHistoricalData = historicalData.filter(data =>
|
}
|
||||||
data &&
|
|
||||||
data.data &&
|
|
||||||
typeof data.data.pv_input_power === 'number' &&
|
|
||||||
!isNaN(data.data.pv_input_power)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validHistoricalData.length === 0) {
|
async function calculateSimplePrediction(weatherData, historicalData) {
|
||||||
throw new Error('No valid historical power data available');
|
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
|
if (!weatherData || !Array.isArray(weatherData)) {
|
||||||
const historicalAvg = validHistoricalData.reduce((sum, data) => sum + data.data.pv_input_power, 0) / validHistoricalData.length;
|
console.error('Invalid weather data:', weatherData);
|
||||||
const basePower = Math.max(200, historicalAvg); // Minimum 200W baseline
|
throw new Error('Invalid weather data provided');
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate predictions for each day
|
const predictions = weatherData.slice(0, 7).map((dayWeather, index) => {
|
||||||
const predictions = weatherData.forecast.forecastday.map(day => {
|
if (!dayWeather) {
|
||||||
// Ensure we have the required data
|
throw new Error(`Weather data missing for day ${index}`);
|
||||||
if (!day || !day.day) {
|
|
||||||
throw new Error('Invalid weather data structure');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloudCover = day.day.cloud || 0;
|
const avgTemp = dayWeather.temp_max && dayWeather.temp_min
|
||||||
const rainChance = day.day.daily_chance_of_rain || 0;
|
? (dayWeather.temp_max + dayWeather.temp_min) / 2
|
||||||
const avgTemp = ((day.day.maxtemp_c || 0) + (day.day.mintemp_c || 0)) / 2;
|
: 20; // Default temp
|
||||||
|
|
||||||
// Validate weather data
|
const cloudImpact = getCloudImpact(dayWeather.description);
|
||||||
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 tempImpact = getTempImpact(avgTemp);
|
const tempImpact = getTempImpact(avgTemp);
|
||||||
const rainImpact = getRainImpact(rainChance);
|
const rainImpact = getRainImpact(dayWeather.description);
|
||||||
|
|
||||||
// Calculate final prediction with all factors
|
|
||||||
const weatherFactor = cloudImpact * tempImpact * rainImpact;
|
const weatherFactor = cloudImpact * tempImpact * rainImpact;
|
||||||
const predictedPower = Math.round(basePower * weatherFactor);
|
const predictedPower = Math.round(basePower * weatherFactor);
|
||||||
|
|
||||||
// Calculate confidence based on weather stability
|
const daysFuture = index + 1;
|
||||||
const weatherStability = 1 - (Math.abs(cloudImpact - 1) + Math.abs(tempImpact - 1) + Math.abs(rainImpact - 1)) / 3;
|
const baseConfidence = 0.9 - (daysFuture * 0.1);
|
||||||
const confidence = 0.7 + (weatherStability * 0.3);
|
const weatherConfidence = (cloudImpact + tempImpact + rainImpact) / 3;
|
||||||
|
const confidence = Math.max(0.3, baseConfidence * weatherConfidence);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: new Date(day.date),
|
date: dayWeather.date ? new Date(dayWeather.date) : new Date(Date.now() + index * 24 * 60 * 60 * 1000),
|
||||||
predictedPower,
|
predictedPower,
|
||||||
weatherData: day,
|
weatherData: dayWeather,
|
||||||
confidence: Math.round(confidence * 100) / 100,
|
confidence: Math.round(confidence * 100) / 100,
|
||||||
factors: {
|
factors: {
|
||||||
cloudImpact,
|
cloudImpact,
|
||||||
tempImpact,
|
tempImpact,
|
||||||
rainImpact
|
rainImpact,
|
||||||
|
weatherFactor
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Generated predictions:', predictions.length);
|
||||||
return predictions;
|
return predictions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request) {
|
||||||
|
console.log('GET /api/predictions called');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { db } = await connectToDatabase();
|
const { db } = await connectToDatabase();
|
||||||
const now = new Date();
|
console.log('Database connected successfully');
|
||||||
|
|
||||||
// Check for cached predictions
|
const { searchParams } = new URL(request.url);
|
||||||
const cachedPredictions = await db.collection('predictions')
|
const forceRefresh = searchParams.get('refresh') === 'true';
|
||||||
.find({
|
console.log('Force refresh:', forceRefresh);
|
||||||
date: { $gte: now },
|
|
||||||
lastUpdated: { $gte: new Date(now.getTime() - CACHE_DURATION) }
|
|
||||||
})
|
|
||||||
.sort({ date: 1 })
|
|
||||||
.toArray();
|
|
||||||
|
|
||||||
if (cachedPredictions.length === 7) {
|
if (!forceRefresh) {
|
||||||
return NextResponse.json(cachedPredictions);
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch fresh weather data
|
const weatherData = await ensureWeatherData(db);
|
||||||
const weatherData = await fetchWeatherData();
|
|
||||||
|
|
||||||
// Get historical power data for the last 30 days
|
if (!weatherData || !weatherData.forecasts) {
|
||||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
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')
|
const historicalData = await db.collection('solar_data')
|
||||||
.find({ timestamp: { $gte: thirtyDaysAgo } })
|
.find({ timestamp: { $gte: thirtyDaysAgo } })
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
// Calculate new predictions
|
console.log('Historical data found:', historicalData.length, 'records');
|
||||||
const predictions = await calculatePrediction(weatherData, historicalData);
|
|
||||||
|
|
||||||
// Store predictions in database
|
console.log('Calculating new predictions...');
|
||||||
const bulkOps = predictions.map(prediction => ({
|
const predictions = await calculateSimplePrediction(weatherData.forecasts, historicalData);
|
||||||
updateOne: {
|
|
||||||
filter: { date: prediction.date },
|
const now = new Date();
|
||||||
update: { $set: prediction },
|
const predictionsWithTimestamp = predictions.map(pred => ({
|
||||||
upsert: true
|
...pred,
|
||||||
}
|
lastUpdated: now
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (bulkOps.length > 0) {
|
console.log('Storing predictions in database...');
|
||||||
await db.collection('predictions').bulkWrite(bulkOps);
|
await db.collection('predictions').deleteMany({});
|
||||||
|
if (predictionsWithTimestamp.length > 0) {
|
||||||
|
await db.collection('predictions').insertMany(predictionsWithTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(predictions);
|
console.log('Predictions stored successfully');
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
predictions: predictionsWithTimestamp,
|
||||||
|
fromCache: false,
|
||||||
|
lastUpdated: now
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Prediction error:', error);
|
console.error('Prediction API error:', error);
|
||||||
return NextResponse.json(
|
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 startDate = new Date()
|
||||||
let endDate = new Date()
|
let endDate = new Date()
|
||||||
|
|
||||||
// If since parameter is provided, use it as the start date
|
|
||||||
if (since) {
|
if (since) {
|
||||||
startDate = new Date(since)
|
startDate = new Date(since)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,23 +8,60 @@ const COORDS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchWeatherFromAPI() {
|
async function fetchWeatherFromAPI() {
|
||||||
|
console.log('Fetching weather from WeatherAPI...')
|
||||||
|
|
||||||
|
if (!process.env.WEATHER_API_KEY) {
|
||||||
|
throw new Error('WEATHER_API_KEY environment variable is not set')
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
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`
|
`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) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch weather data')
|
const errorText = await response.text()
|
||||||
|
console.error('WeatherAPI Error:', response.status, errorText)
|
||||||
|
throw new Error(`Weather API failed: ${response.status} - ${errorText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json()
|
const data = await response.json()
|
||||||
|
console.log('Weather data fetched successfully:', !!data.forecast?.forecastday)
|
||||||
|
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
function convertToInternalFormat(weatherApiData) {
|
||||||
try {
|
if (!weatherApiData.forecast?.forecastday) {
|
||||||
// Ensure database connection
|
throw new Error('Invalid weather data structure from WeatherAPI')
|
||||||
const { db } = await connectToDatabase()
|
}
|
||||||
|
|
||||||
|
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({
|
const cachedWeather = await db.collection('weather').findOne({
|
||||||
'location.lat': COORDS.lat,
|
'location.lat': COORDS.lat,
|
||||||
'location.lon': COORDS.lon
|
'location.lon': COORDS.lon
|
||||||
@@ -32,43 +69,84 @@ export async function GET() {
|
|||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const isCacheValid = cachedWeather &&
|
const isCacheValid = cachedWeather &&
|
||||||
(now - new Date(cachedWeather.lastUpdated)) < CACHE_DURATION
|
(now - new Date(cachedWeather.lastUpdated)) < CACHE_DURATION
|
||||||
|
|
||||||
// If cache is valid, return cached data
|
console.log('Cache status:', {
|
||||||
if (isCacheValid) {
|
hasCached: !!cachedWeather,
|
||||||
|
isCacheValid,
|
||||||
|
forceRefresh
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isCacheValid && !forceRefresh) {
|
||||||
|
console.log('Returning cached weather data')
|
||||||
return NextResponse.json(cachedWeather)
|
return NextResponse.json(cachedWeather)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no cache or cache is invalid, fetch fresh data
|
console.log('Fetching fresh weather data...')
|
||||||
const weatherData = await fetchWeatherFromAPI()
|
const weatherData = await fetchWeatherFromAPI()
|
||||||
|
|
||||||
// Add metadata to the weather data
|
|
||||||
const weatherWithMetadata = {
|
const weatherWithMetadata = {
|
||||||
...weatherData,
|
...weatherData,
|
||||||
location: COORDS,
|
location: COORDS,
|
||||||
lastUpdated: now
|
lastUpdated: now
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update or create weather document
|
|
||||||
await db.collection('weather').updateOne(
|
await db.collection('weather').updateOne(
|
||||||
{
|
{
|
||||||
'location.lat': COORDS.lat,
|
'location.lat': COORDS.lat,
|
||||||
'location.lon': COORDS.lon
|
'location.lon': COORDS.lon
|
||||||
},
|
},
|
||||||
{ $set: weatherWithMetadata },
|
{ $set: weatherWithMetadata },
|
||||||
{ upsert: true }
|
{ 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)
|
return NextResponse.json(weatherWithMetadata)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Weather API Error:', error)
|
console.error('Weather API Error:', error)
|
||||||
// If we have cached data but failed to fetch new data, return cached data
|
|
||||||
if (cachedWeather) {
|
try {
|
||||||
return NextResponse.json(cachedWeather)
|
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(
|
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 { PowerStats } from '@/components/PowerStats'
|
||||||
import { SystemStatus } from '@/components/SystemStatus'
|
import { SystemStatus } from '@/components/SystemStatus'
|
||||||
import { SolarChart } from '@/components/SolarChart'
|
import { SolarChart } from '@/components/SolarChart'
|
||||||
import { LastRefresh } from '@/components/LastRefresh'
|
import { motion } from 'framer-motion'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
|
||||||
import { Navbar } from '@/components/Navbar'
|
import { Navbar } from '@/components/Navbar'
|
||||||
import WeatherForecast from '@/components/WeatherForecast'
|
import WeatherForecast from '@/components/WeatherForecast'
|
||||||
import PowerPredictionGraph from '@/components/PowerPredictionGraph'
|
import PowerPredictionGraph from '@/components/PowerPredictionGraph'
|
||||||
@@ -32,7 +30,6 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
if (newData.length > 0) {
|
if (newData.length > 0) {
|
||||||
setCurrentData(prevData => {
|
setCurrentData(prevData => {
|
||||||
// Merge new data with existing data, avoiding duplicates
|
|
||||||
const mergedData = [...prevData]
|
const mergedData = [...prevData]
|
||||||
newData.forEach(newItem => {
|
newData.forEach(newItem => {
|
||||||
const newTimestamp = newItem.timestamp.$date || newItem.timestamp
|
const newTimestamp = newItem.timestamp.$date || newItem.timestamp
|
||||||
@@ -48,11 +45,9 @@ export default function Dashboard() {
|
|||||||
return mergedData
|
return mergedData
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update last update time
|
|
||||||
const latestTimestamp = newData[newData.length - 1].timestamp.$date || newData[newData.length - 1].timestamp
|
const latestTimestamp = newData[newData.length - 1].timestamp.$date || newData[newData.length - 1].timestamp
|
||||||
setLastUpdateTime(latestTimestamp)
|
setLastUpdateTime(latestTimestamp)
|
||||||
|
|
||||||
// Update chart data if viewing today's data
|
|
||||||
if (chartTimeRange === 'today') {
|
if (chartTimeRange === 'today') {
|
||||||
setData(prevData => {
|
setData(prevData => {
|
||||||
const mergedData = [...prevData]
|
const mergedData = [...prevData]
|
||||||
@@ -78,7 +73,7 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const fetchHistoricalData = async () => {
|
const fetchHistoricalData = async () => {
|
||||||
try {
|
try {
|
||||||
setLastUpdateTime(null) // Reset last update time when changing time range
|
setLastUpdateTime(null);
|
||||||
const response = await fetch(`/api/solar-data?timeRange=${chartTimeRange}`)
|
const response = await fetch(`/api/solar-data?timeRange=${chartTimeRange}`)
|
||||||
const newData = await response.json()
|
const newData = await response.json()
|
||||||
setData(newData)
|
setData(newData)
|
||||||
@@ -90,7 +85,6 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set minimum loading time
|
|
||||||
const minLoadingTimer = setTimeout(() => {
|
const minLoadingTimer = setTimeout(() => {
|
||||||
setMinLoadingComplete(true)
|
setMinLoadingComplete(true)
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|||||||
@@ -13,121 +13,310 @@ import {
|
|||||||
Legend
|
Legend
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { RefreshCw, Clock, Database, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
export default function PowerPredictionGraph() {
|
export default function PowerPredictionGraph() {
|
||||||
const [predictions, setPredictions] = useState(null);
|
const [predictions, setPredictions] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchPredictions = async () => {
|
console.log('Component mounted, fetching initial predictions');
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchPredictions();
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700">
|
<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="flex items-center justify-center h-64">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0.5, opacity: 0 }}
|
initial={{ scale: 0.5, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="relative"
|
className="relative"
|
||||||
>
|
>
|
||||||
<div className="w-16 h-16 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
<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="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 className="w-12 h-12 border-4 border-emerald-500 border-t-transparent rounded-full animate-spin" style={{ animationDirection: 'reverse' }}></div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700">
|
<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="flex flex-col items-center justify-center h-64 space-y-4">
|
||||||
<div className="text-red-500">Error loading predictions</div>
|
<div className="text-red-500 text-center">
|
||||||
</div>
|
<AlertCircle className="w-12 h-12 mx-auto mb-2" />
|
||||||
</Card>
|
<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) {
|
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 => ({
|
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,
|
predicted: pred.predictedPower,
|
||||||
confidence: pred.confidence * 100
|
confidence: pred.confidence * 100
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
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">
|
<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>
|
{/* Header with title and refresh button */}
|
||||||
<div className="h-64">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<h2 className="text-lg font-semibold text-white">7-Day Power Generation Forecast</h2>
|
||||||
<LineChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
<div className="flex items-center space-x-3">
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
{/* Cache indicator */}
|
||||||
<XAxis
|
<div className="flex items-center space-x-1 text-xs text-gray-400">
|
||||||
dataKey="date"
|
{fromCache ? (
|
||||||
stroke="#9CA3AF"
|
<>
|
||||||
tick={{ fill: '#9CA3AF' }}
|
<Database className="w-3 h-3" />
|
||||||
/>
|
<span>Cached</span>
|
||||||
<YAxis
|
</>
|
||||||
stroke="#9CA3AF"
|
) : (
|
||||||
tick={{ fill: '#9CA3AF' }}
|
<>
|
||||||
label={{
|
<RefreshCw className="w-3 h-3" />
|
||||||
value: 'Power (W)',
|
<span>Fresh</span>
|
||||||
angle: -90,
|
</>
|
||||||
position: 'insideLeft',
|
)}
|
||||||
fill: '#9CA3AF'
|
</div>
|
||||||
}}
|
|
||||||
/>
|
{/* Last updated */}
|
||||||
<Tooltip
|
<div className="flex items-center space-x-1 text-xs text-gray-400">
|
||||||
contentStyle={{
|
<Clock className="w-3 h-3" />
|
||||||
backgroundColor: '#1F2937',
|
<span>{formatLastUpdated(lastUpdated)}</span>
|
||||||
border: '1px solid #374151',
|
</div>
|
||||||
borderRadius: '0.375rem',
|
|
||||||
color: '#F3F4F6'
|
{/* Refresh button */}
|
||||||
}}
|
<button
|
||||||
/>
|
onClick={handleRefresh}
|
||||||
<Legend />
|
disabled={refreshing}
|
||||||
<Line
|
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"
|
||||||
type="monotone"
|
>
|
||||||
dataKey="predicted"
|
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||||
name="Predicted Power"
|
<span>{refreshing ? 'Updating...' : 'Refresh'}</span>
|
||||||
stroke="#60A5FA"
|
</button>
|
||||||
strokeWidth={2}
|
</div>
|
||||||
dot={{ fill: '#60A5FA', strokeWidth: 2 }}
|
</div>
|
||||||
activeDot={{ r: 8 }}
|
|
||||||
/>
|
<div className="h-64">
|
||||||
<Line
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
type="monotone"
|
<LineChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
dataKey="confidence"
|
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||||
name="Confidence %"
|
<XAxis
|
||||||
stroke="#34D399"
|
dataKey="date"
|
||||||
strokeWidth={2}
|
stroke="#9CA3AF"
|
||||||
dot={{ fill: '#34D399', strokeWidth: 2 }}
|
tick={{ fill: '#9CA3AF', fontSize: 12 }}
|
||||||
activeDot={{ r: 8 }}
|
/>
|
||||||
/>
|
<YAxis
|
||||||
</LineChart>
|
stroke="#9CA3AF"
|
||||||
</ResponsiveContainer>
|
tick={{ fill: '#9CA3AF', fontSize: 12 }}
|
||||||
</div>
|
label={{
|
||||||
</Card>
|
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) => {
|
const formatData = (data) => {
|
||||||
return getFilteredData(data).map(item => {
|
return getFilteredData(data).map(item => {
|
||||||
// Handle both MongoDB date format and regular date string
|
|
||||||
const dateStr = item.timestamp.$date || item.timestamp
|
const dateStr = item.timestamp.$date || item.timestamp
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
|
|
||||||
// Check if date is valid
|
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) {
|
||||||
console.error('Invalid date:', dateStr)
|
console.error('Invalid date:', dateStr)
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -8,23 +8,70 @@ const predictionSchema = new mongoose.Schema({
|
|||||||
},
|
},
|
||||||
predictedPower: {
|
predictedPower: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: true,
|
||||||
|
min: 0
|
||||||
},
|
},
|
||||||
weatherData: {
|
weatherData: {
|
||||||
type: Object,
|
date: String,
|
||||||
required: true
|
temp_min: Number,
|
||||||
|
temp_max: Number,
|
||||||
|
humidity: Number,
|
||||||
|
wind_speed: Number,
|
||||||
|
icon: String,
|
||||||
|
description: String,
|
||||||
|
timestamp: Date
|
||||||
},
|
},
|
||||||
confidence: {
|
confidence: {
|
||||||
type: Number,
|
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: {
|
lastUpdated: {
|
||||||
type: Date,
|
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 });
|
|
||||||
|
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);
|
export const Prediction = mongoose.models.Prediction || mongoose.model('Prediction', predictionSchema);
|
||||||
@@ -11,15 +11,12 @@ let client
|
|||||||
let clientPromise
|
let clientPromise
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
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) {
|
if (!global._mongoClientPromise) {
|
||||||
client = new MongoClient(uri, options)
|
client = new MongoClient(uri, options)
|
||||||
global._mongoClientPromise = client.connect()
|
global._mongoClientPromise = client.connect()
|
||||||
}
|
}
|
||||||
clientPromise = global._mongoClientPromise
|
clientPromise = global._mongoClientPromise
|
||||||
} else {
|
} else {
|
||||||
// In production mode, it's best to not use a global variable.
|
|
||||||
client = new MongoClient(uri, options)
|
client = new MongoClient(uri, options)
|
||||||
clientPromise = client.connect()
|
clientPromise = client.connect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export async function fetchWeatherData() {
|
|||||||
)
|
)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
// Process the data to get daily forecasts
|
|
||||||
const dailyForecasts = data.list.reduce((acc, item) => {
|
const dailyForecasts = data.list.reduce((acc, item) => {
|
||||||
const date = new Date(item.dt * 1000)
|
const date = new Date(item.dt * 1000)
|
||||||
const day = date.toISOString().split('T')[0]
|
const day = date.toISOString().split('T')[0]
|
||||||
@@ -67,10 +66,8 @@ export async function getWeatherData() {
|
|||||||
const { db } = await connectToDatabase()
|
const { db } = await connectToDatabase()
|
||||||
const collection = db.collection('weather_data')
|
const collection = db.collection('weather_data')
|
||||||
|
|
||||||
// Get the latest weather data
|
|
||||||
const latestData = await collection.findOne({}, { sort: { timestamp: -1 } })
|
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)) {
|
if (!latestData || (Date.now() - latestData.timestamp.getTime() > 2 * 60 * 60 * 1000)) {
|
||||||
const newData = await fetchWeatherData()
|
const newData = await fetchWeatherData()
|
||||||
await collection.insertOne({
|
await collection.insertOne({
|
||||||
|
|||||||
Reference in New Issue
Block a user