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": { "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",

View File

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

View File

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

View File

@@ -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;
if (temp > 35) return WEATHER_IMPACT.TEMP_IMPACT.HOT;
if (temp < 20) {
return 0.9 + ((temp - 10) / 10) * 0.1;
} else { } else {
return 1.0 - ((temp - 25) * 0.01); // 1% loss per degree above optimal 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();
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.HEAVY;
} }
return WEATHER_IMPACT.RAIN_IMPACT.LIGHT;
async function calculatePrediction(weatherData, historicalData) { }
// Validate historical data if (desc.includes('snow') || desc.includes('thunderstorm')) {
if (!historicalData || historicalData.length === 0) { return WEATHER_IMPACT.RAIN_IMPACT.HEAVY;
throw new Error('No historical data available for predictions'); }
return WEATHER_IMPACT.RAIN_IMPACT.NONE;
} }
// Filter out invalid data points and calculate average async function ensureWeatherData(db) {
const validHistoricalData = historicalData.filter(data => console.log('Checking for weather data...');
data &&
data.data && let weatherData = await db.collection('weather_data')
typeof data.data.pv_input_power === 'number' && .findOne({}, { sort: { timestamp: -1 } });
!isNaN(data.data.pv_input_power)
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);
}
}
return weatherData;
}
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 (validHistoricalData.length === 0) { if (validData.length > 0) {
throw new Error('No valid historical power data available'); 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.forecast.forecastday.map(day => {
// Ensure we have the required data
if (!day || !day.day) {
throw new Error('Invalid weather data structure');
} }
const cloudCover = day.day.cloud || 0; const predictions = weatherData.slice(0, 7).map((dayWeather, index) => {
const rainChance = day.day.daily_chance_of_rain || 0; if (!dayWeather) {
const avgTemp = ((day.day.maxtemp_c || 0) + (day.day.mintemp_c || 0)) / 2; throw new Error(`Weather data missing for day ${index}`);
// 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 avgTemp = dayWeather.temp_max && dayWeather.temp_min
const cloudImpact = getCloudImpact(cloudCover); ? (dayWeather.temp_max + dayWeather.temp_min) / 2
: 20; // Default temp
const cloudImpact = getCloudImpact(dayWeather.description);
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) } if (!forceRefresh) {
}) console.log('Checking for cached predictions...');
const existingPredictions = await db.collection('predictions')
.find({})
.sort({ date: 1 }) .sort({ date: 1 })
.toArray(); .toArray();
if (cachedPredictions.length === 7) { console.log('Found cached predictions:', existingPredictions.length);
return NextResponse.json(cachedPredictions);
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' }, {
error: error.message || 'Failed to generate predictions',
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
timestamp: new Date().toISOString()
},
{ status: 500 } { 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 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 {

View File

@@ -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) {
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 { try {
// Ensure database connection const { searchParams } = new URL(request.url)
const { db } = await connectToDatabase() 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
@@ -34,22 +71,26 @@ export async function GET() {
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,
@@ -59,15 +100,52 @@ export async function GET() {
{ 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
try {
const { db } = await connectToDatabase()
const cachedWeather = await db.collection('weather').findOne({
'location.lat': COORDS.lat,
'location.lon': COORDS.lon
})
if (cachedWeather) { if (cachedWeather) {
return NextResponse.json(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' }, {
error: 'Failed to fetch weather data',
details: error.message,
timestamp: new Date().toISOString()
},
{ status: 500 } { status: 500 }
) )
} }

View File

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

View File

@@ -13,29 +13,101 @@ 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 = [];
useEffect(() => {
const fetchPredictions = async () => {
try { try {
const response = await fetch('/api/predictions'); debugLog.push(`Starting fetch - forceRefresh: ${forceRefresh}`);
if (!response.ok) throw new Error('Failed to fetch predictions');
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(); const data = await response.json();
setPredictions(data); 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) { } catch (err) {
debugLog.push(`Error caught: ${err.message}`);
console.error('Fetch error:', err);
setError(err.message); setError(err.message);
} finally { } finally {
setLoading(false); setLoading(false);
setRefreshing(false);
setDebugInfo(debugLog.join('\n'));
debugLog.push('Loading states reset');
} }
}; };
useEffect(() => {
console.log('Component mounted, fetching initial predictions');
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">
@@ -59,26 +131,104 @@ export default function PowerPredictionGraph() {
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">
<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> </div>
</Card> </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="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"> <div className="h-64">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}> <LineChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
@@ -86,11 +236,11 @@ export default function PowerPredictionGraph() {
<XAxis <XAxis
dataKey="date" dataKey="date"
stroke="#9CA3AF" stroke="#9CA3AF"
tick={{ fill: '#9CA3AF' }} tick={{ fill: '#9CA3AF', fontSize: 12 }}
/> />
<YAxis <YAxis
stroke="#9CA3AF" stroke="#9CA3AF"
tick={{ fill: '#9CA3AF' }} tick={{ fill: '#9CA3AF', fontSize: 12 }}
label={{ label={{
value: 'Power (W)', value: 'Power (W)',
angle: -90, angle: -90,
@@ -105,6 +255,10 @@ export default function PowerPredictionGraph() {
borderRadius: '0.375rem', borderRadius: '0.375rem',
color: '#F3F4F6' color: '#F3F4F6'
}} }}
formatter={(value, name) => [
`${value}${name === 'Predicted Power' ? 'W' : '%'}`,
name === 'Predicted Power' ? 'Predicted Power' : 'Confidence'
]}
/> />
<Legend /> <Legend />
<Line <Line
@@ -114,7 +268,7 @@ export default function PowerPredictionGraph() {
stroke="#60A5FA" stroke="#60A5FA"
strokeWidth={2} strokeWidth={2}
dot={{ fill: '#60A5FA', strokeWidth: 2 }} dot={{ fill: '#60A5FA', strokeWidth: 2 }}
activeDot={{ r: 8 }} activeDot={{ r: 6 }}
/> />
<Line <Line
type="monotone" type="monotone"
@@ -123,11 +277,46 @@ export default function PowerPredictionGraph() {
stroke="#34D399" stroke="#34D399"
strokeWidth={2} strokeWidth={2}
dot={{ fill: '#34D399', strokeWidth: 2 }} dot={{ fill: '#34D399', strokeWidth: 2 }}
activeDot={{ r: 8 }} activeDot={{ r: 6 }}
/> />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </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> </Card>
); );
} }

View File

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

View File

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

View File

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

View File

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