working prediction
This commit is contained in:
@@ -3,6 +3,30 @@ 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
|
||||
},
|
||||
// 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
|
||||
},
|
||||
// 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
|
||||
}
|
||||
};
|
||||
|
||||
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`
|
||||
@@ -11,30 +35,94 @@ async function fetchWeatherData() {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
async function calculatePrediction(weatherData, historicalData) {
|
||||
// Simple prediction model based on weather conditions and historical data
|
||||
// Validate historical data
|
||||
if (!historicalData || historicalData.length === 0) {
|
||||
throw new Error('No historical data available for predictions');
|
||||
}
|
||||
|
||||
// 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)
|
||||
);
|
||||
|
||||
if (validHistoricalData.length === 0) {
|
||||
throw new Error('No valid historical power data available');
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Calculate predictions for each day
|
||||
const predictions = weatherData.forecast.forecastday.map(day => {
|
||||
const cloudCover = day.day.cloud;
|
||||
const rainChance = day.day.daily_chance_of_rain;
|
||||
const avgTemp = (day.day.maxtemp_c + day.day.mintemp_c) / 2;
|
||||
// Ensure we have the required data
|
||||
if (!day || !day.day) {
|
||||
throw new Error('Invalid weather data structure');
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Base prediction on historical average
|
||||
const historicalAvg = historicalData.reduce((sum, data) => sum + data.power, 0) / historicalData.length;
|
||||
// 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');
|
||||
}
|
||||
|
||||
// Adjust based on weather conditions
|
||||
let weatherFactor = 1.0;
|
||||
weatherFactor *= (100 - cloudCover) / 100; // Less clouds = more power
|
||||
weatherFactor *= (100 - rainChance) / 100; // Less rain = more power
|
||||
weatherFactor *= Math.min(1, 1 - Math.abs(avgTemp - 25) / 50); // Optimal temperature around 25°C
|
||||
// Calculate weather impact factors
|
||||
const cloudImpact = getCloudImpact(cloudCover);
|
||||
const tempImpact = getTempImpact(avgTemp);
|
||||
const rainImpact = getRainImpact(rainChance);
|
||||
|
||||
const predictedPower = historicalAvg * weatherFactor;
|
||||
const confidence = 0.7 + (weatherFactor * 0.3); // Higher confidence for better weather conditions
|
||||
// Calculate final prediction with all factors
|
||||
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);
|
||||
|
||||
return {
|
||||
date: new Date(day.date),
|
||||
predictedPower: Math.round(predictedPower * 100) / 100,
|
||||
predictedPower,
|
||||
weatherData: day,
|
||||
confidence: Math.round(confidence * 100) / 100
|
||||
confidence: Math.round(confidence * 100) / 100,
|
||||
factors: {
|
||||
cloudImpact,
|
||||
tempImpact,
|
||||
rainImpact
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -64,7 +152,7 @@ export async function GET() {
|
||||
|
||||
// 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('power')
|
||||
const historicalData = await db.collection('solar_data')
|
||||
.find({ timestamp: { $gte: thirtyDaysAgo } })
|
||||
.toArray();
|
||||
|
||||
@@ -88,7 +176,7 @@ export async function GET() {
|
||||
} catch (error) {
|
||||
console.error('Prediction error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to generate predictions' },
|
||||
{ error: error.message || 'Failed to generate predictions' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import PowerPredictionGraph from '@/components/PowerPredictionGraph'
|
||||
export default function Dashboard() {
|
||||
const [data, setData] = useState([])
|
||||
const [currentData, setCurrentData] = useState([])
|
||||
const [timeRange, setTimeRange] = useState('today')
|
||||
const [chartTimeRange, setChartTimeRange] = useState('today')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [minLoadingComplete, setMinLoadingComplete] = useState(false)
|
||||
const [lastUpdateTime, setLastUpdateTime] = useState(null)
|
||||
@@ -52,8 +52,8 @@ export default function Dashboard() {
|
||||
const latestTimestamp = newData[newData.length - 1].timestamp.$date || newData[newData.length - 1].timestamp
|
||||
setLastUpdateTime(latestTimestamp)
|
||||
|
||||
// Update historical data if viewing today
|
||||
if (timeRange === 'today') {
|
||||
// Update chart data if viewing today's data
|
||||
if (chartTimeRange === 'today') {
|
||||
setData(prevData => {
|
||||
const mergedData = [...prevData]
|
||||
newData.forEach(newItem => {
|
||||
@@ -79,10 +79,9 @@ export default function Dashboard() {
|
||||
const fetchHistoricalData = async () => {
|
||||
try {
|
||||
setLastUpdateTime(null) // Reset last update time when changing time range
|
||||
const response = await fetch(`/api/solar-data?timeRange=${timeRange}`)
|
||||
const response = await fetch(`/api/solar-data?timeRange=${chartTimeRange}`)
|
||||
const newData = await response.json()
|
||||
setData(newData)
|
||||
setCurrentData(newData)
|
||||
} catch (error) {
|
||||
console.error('Error fetching historical data:', error)
|
||||
} finally {
|
||||
@@ -107,7 +106,7 @@ export default function Dashboard() {
|
||||
|
||||
useEffect(() => {
|
||||
fetchHistoricalData()
|
||||
}, [timeRange])
|
||||
}, [chartTimeRange])
|
||||
|
||||
if (loading || !minLoadingComplete) {
|
||||
return (
|
||||
@@ -198,22 +197,22 @@ export default function Dashboard() {
|
||||
>
|
||||
<motion.div variants={itemVariants} className="h-full">
|
||||
<Card className="h-full p-4 sm:p-6 bg-gray-800/50 backdrop-blur-sm border-gray-700 hover:bg-gray-800/70 transition-all duration-300 transform hover:scale-[1.02] hover:shadow-2xl hover:shadow-purple-500/10">
|
||||
<SolarChart data={data} type="voltage" timeRange={timeRange} onTimeRangeChange={setTimeRange} />
|
||||
<SolarChart data={data} type="voltage" timeRange={chartTimeRange} onTimeRangeChange={setChartTimeRange} />
|
||||
</Card>
|
||||
</motion.div>
|
||||
<motion.div variants={itemVariants} className="h-full">
|
||||
<Card className="h-full p-4 sm:p-6 bg-gray-800/50 backdrop-blur-sm border-gray-700 hover:bg-gray-800/70 transition-all duration-300 transform hover:scale-[1.02] hover:shadow-2xl hover:shadow-blue-500/10">
|
||||
<SolarChart data={data} type="power" timeRange={timeRange} onTimeRangeChange={setTimeRange} />
|
||||
<SolarChart data={data} type="power" timeRange={chartTimeRange} onTimeRangeChange={setChartTimeRange} />
|
||||
</Card>
|
||||
</motion.div>
|
||||
<motion.div variants={itemVariants} className="h-full">
|
||||
<Card className="h-full p-4 sm:p-6 bg-gray-800/50 backdrop-blur-sm border-gray-700 hover:bg-gray-800/70 transition-all duration-300 transform hover:scale-[1.02] hover:shadow-2xl hover:shadow-amber-500/10">
|
||||
<SolarChart data={data} type="current" timeRange={timeRange} onTimeRangeChange={setTimeRange} />
|
||||
<SolarChart data={data} type="current" timeRange={chartTimeRange} onTimeRangeChange={setChartTimeRange} />
|
||||
</Card>
|
||||
</motion.div>
|
||||
<motion.div variants={itemVariants} className="h-full">
|
||||
<Card className="h-full p-4 sm:p-6 bg-gray-800/50 backdrop-blur-sm border-gray-700 hover:bg-gray-800/70 transition-all duration-300 transform hover:scale-[1.02] hover:shadow-2xl hover:shadow-red-500/10">
|
||||
<SolarChart data={data} type="temperature" timeRange={timeRange} onTimeRangeChange={setTimeRange} />
|
||||
<SolarChart data={data} type="temperature" timeRange={chartTimeRange} onTimeRangeChange={setChartTimeRange} />
|
||||
</Card>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ResponsiveContainer,
|
||||
Legend
|
||||
} from 'recharts';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export default function PowerPredictionGraph() {
|
||||
const [predictions, setPredictions] = useState(null);
|
||||
@@ -37,17 +38,31 @@ export default function PowerPredictionGraph() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full h-64 bg-gray-100 rounded-lg animate-pulse flex items-center justify-center">
|
||||
<div className="text-gray-400">Loading predictions...</div>
|
||||
</div>
|
||||
<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 (
|
||||
<div className="w-full h-64 bg-red-50 rounded-lg flex items-center justify-center">
|
||||
<div className="text-red-500">Error loading predictions</div>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +92,7 @@ export default function PowerPredictionGraph() {
|
||||
stroke="#9CA3AF"
|
||||
tick={{ fill: '#9CA3AF' }}
|
||||
label={{
|
||||
value: 'Power (kW)',
|
||||
value: 'Power (W)',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
fill: '#9CA3AF'
|
||||
|
||||
@@ -50,17 +50,31 @@ export default function WeatherForecast() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full h-24 bg-gray-100 rounded-lg animate-pulse flex items-center justify-center">
|
||||
<div className="text-gray-400">Loading weather data...</div>
|
||||
</div>
|
||||
<Card className="w-full p-3 bg-gray-800/50 backdrop-blur-sm border-gray-700">
|
||||
<div className="flex items-center justify-center h-24">
|
||||
<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 (
|
||||
<div className="w-full h-24 bg-red-50 rounded-lg flex items-center justify-center">
|
||||
<div className="text-red-500">Error loading weather data</div>
|
||||
</div>
|
||||
<Card className="w-full p-3 bg-gray-800/50 backdrop-blur-sm border-gray-700">
|
||||
<div className="flex items-center justify-center h-24">
|
||||
<div className="text-red-500">Error loading weather data</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user