working prediction

This commit is contained in:
lumijiez
2025-05-22 22:48:51 +03:00
parent e2db9ed618
commit fe5fe3da66
4 changed files with 156 additions and 40 deletions

View File

@@ -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');
}
// Base prediction on historical average
const historicalAvg = historicalData.reduce((sum, data) => sum + data.power, 0) / historicalData.length;
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;
// 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
// 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');
}
const predictedPower = historicalAvg * weatherFactor;
const confidence = 0.7 + (weatherFactor * 0.3); // Higher confidence for better weather conditions
// Calculate weather impact factors
const cloudImpact = getCloudImpact(cloudCover);
const tempImpact = getTempImpact(avgTemp);
const rainImpact = getRainImpact(rainChance);
// 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 }
);
}

View File

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

View File

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

View File

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