This commit is contained in:
lumijiez
2025-06-16 01:12:22 +03:00
parent 4383c6963f
commit f21280ff79
12 changed files with 661 additions and 284 deletions

10
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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()
}

View File

@@ -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'));
}

View File

@@ -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 {

View File

@@ -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 }
)
}
}
}

View File

@@ -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)

View File

@@ -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>
);
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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()
}

View File

@@ -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({