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 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() { async function fetchWeatherData() {
const response = await fetch( 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` `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(); 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) { 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 predictions = weatherData.forecast.forecastday.map(day => {
const cloudCover = day.day.cloud; // Ensure we have the required data
const rainChance = day.day.daily_chance_of_rain; if (!day || !day.day) {
const avgTemp = (day.day.maxtemp_c + day.day.mintemp_c) / 2; throw new Error('Invalid weather data structure');
}
// Base prediction on historical average const cloudCover = day.day.cloud || 0;
const historicalAvg = historicalData.reduce((sum, data) => sum + data.power, 0) / historicalData.length; 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 // Validate weather data
let weatherFactor = 1.0; if (typeof cloudCover !== 'number' || typeof rainChance !== 'number' || typeof avgTemp !== 'number') {
weatherFactor *= (100 - cloudCover) / 100; // Less clouds = more power console.error('Weather data validation failed:', { cloudCover, rainChance, avgTemp });
weatherFactor *= (100 - rainChance) / 100; // Less rain = more power throw new Error('Invalid weather data received');
weatherFactor *= Math.min(1, 1 - Math.abs(avgTemp - 25) / 50); // Optimal temperature around 25°C }
const predictedPower = historicalAvg * weatherFactor; // Calculate weather impact factors
const confidence = 0.7 + (weatherFactor * 0.3); // Higher confidence for better weather conditions 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 { return {
date: new Date(day.date), date: new Date(day.date),
predictedPower: Math.round(predictedPower * 100) / 100, predictedPower,
weatherData: day, 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 // Get historical power data for the last 30 days
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); 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 } }) .find({ timestamp: { $gte: thirtyDaysAgo } })
.toArray(); .toArray();
@@ -88,7 +176,7 @@ export async function GET() {
} catch (error) { } catch (error) {
console.error('Prediction error:', error); console.error('Prediction error:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to generate predictions' }, { error: error.message || 'Failed to generate predictions' },
{ status: 500 } { status: 500 }
); );
} }

View File

@@ -16,7 +16,7 @@ import PowerPredictionGraph from '@/components/PowerPredictionGraph'
export default function Dashboard() { export default function Dashboard() {
const [data, setData] = useState([]) const [data, setData] = useState([])
const [currentData, setCurrentData] = useState([]) const [currentData, setCurrentData] = useState([])
const [timeRange, setTimeRange] = useState('today') const [chartTimeRange, setChartTimeRange] = useState('today')
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [minLoadingComplete, setMinLoadingComplete] = useState(false) const [minLoadingComplete, setMinLoadingComplete] = useState(false)
const [lastUpdateTime, setLastUpdateTime] = useState(null) 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 const latestTimestamp = newData[newData.length - 1].timestamp.$date || newData[newData.length - 1].timestamp
setLastUpdateTime(latestTimestamp) setLastUpdateTime(latestTimestamp)
// Update historical data if viewing today // Update chart data if viewing today's data
if (timeRange === 'today') { if (chartTimeRange === 'today') {
setData(prevData => { setData(prevData => {
const mergedData = [...prevData] const mergedData = [...prevData]
newData.forEach(newItem => { newData.forEach(newItem => {
@@ -79,10 +79,9 @@ export default function Dashboard() {
const fetchHistoricalData = async () => { const fetchHistoricalData = async () => {
try { try {
setLastUpdateTime(null) // Reset last update time when changing time range 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() const newData = await response.json()
setData(newData) setData(newData)
setCurrentData(newData)
} catch (error) { } catch (error) {
console.error('Error fetching historical data:', error) console.error('Error fetching historical data:', error)
} finally { } finally {
@@ -107,7 +106,7 @@ export default function Dashboard() {
useEffect(() => { useEffect(() => {
fetchHistoricalData() fetchHistoricalData()
}, [timeRange]) }, [chartTimeRange])
if (loading || !minLoadingComplete) { if (loading || !minLoadingComplete) {
return ( return (
@@ -198,22 +197,22 @@ export default function Dashboard() {
> >
<motion.div variants={itemVariants} className="h-full"> <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"> <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> </Card>
</motion.div> </motion.div>
<motion.div variants={itemVariants} className="h-full"> <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"> <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> </Card>
</motion.div> </motion.div>
<motion.div variants={itemVariants} className="h-full"> <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"> <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> </Card>
</motion.div> </motion.div>
<motion.div variants={itemVariants} className="h-full"> <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"> <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> </Card>
</motion.div> </motion.div>
</motion.div> </motion.div>

View File

@@ -12,6 +12,7 @@ import {
ResponsiveContainer, ResponsiveContainer,
Legend Legend
} from 'recharts'; } from 'recharts';
import { motion } from 'framer-motion';
export default function PowerPredictionGraph() { export default function PowerPredictionGraph() {
const [predictions, setPredictions] = useState(null); const [predictions, setPredictions] = useState(null);
@@ -37,17 +38,31 @@ export default function PowerPredictionGraph() {
if (loading) { if (loading) {
return ( return (
<div className="w-full h-64 bg-gray-100 rounded-lg animate-pulse flex items-center justify-center"> <Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700">
<div className="text-gray-400">Loading predictions...</div> <div className="flex items-center justify-center h-64">
</div> <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) { if (error) {
return ( return (
<div className="w-full h-64 bg-red-50 rounded-lg flex items-center justify-center"> <Card className="w-full p-4 bg-gray-800/50 backdrop-blur-sm border-gray-700">
<div className="text-red-500">Error loading predictions</div> <div className="flex items-center justify-center h-64">
</div> <div className="text-red-500">Error loading predictions</div>
</div>
</Card>
); );
} }
@@ -77,7 +92,7 @@ export default function PowerPredictionGraph() {
stroke="#9CA3AF" stroke="#9CA3AF"
tick={{ fill: '#9CA3AF' }} tick={{ fill: '#9CA3AF' }}
label={{ label={{
value: 'Power (kW)', value: 'Power (W)',
angle: -90, angle: -90,
position: 'insideLeft', position: 'insideLeft',
fill: '#9CA3AF' fill: '#9CA3AF'

View File

@@ -50,17 +50,31 @@ export default function WeatherForecast() {
if (loading) { if (loading) {
return ( return (
<div className="w-full h-24 bg-gray-100 rounded-lg animate-pulse flex items-center justify-center"> <Card className="w-full p-3 bg-gray-800/50 backdrop-blur-sm border-gray-700">
<div className="text-gray-400">Loading weather data...</div> <div className="flex items-center justify-center h-24">
</div> <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) { if (error) {
return ( return (
<div className="w-full h-24 bg-red-50 rounded-lg flex items-center justify-center"> <Card className="w-full p-3 bg-gray-800/50 backdrop-blur-sm border-gray-700">
<div className="text-red-500">Error loading weather data</div> <div className="flex items-center justify-center h-24">
</div> <div className="text-red-500">Error loading weather data</div>
</div>
</Card>
); );
} }