decharge-scout 2.5.6 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +18 -0
- package/.env.example +12 -1
- package/README.md +93 -29
- package/dashboard/api/alpha.js +167 -0
- package/dashboard/api/submit.js +23 -0
- package/dashboard/lib/migration-alpha-contributions.sql +185 -0
- package/dashboard/public/index.html +236 -0
- package/index.js +200 -89
- package/package.json +8 -3
- package/src/energy-data.js +264 -8
- package/src/local-alpha.js +325 -0
- package/src/smart-pricing.js +251 -0
- package/src/weather-data.js +117 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Pricing Simulation Engine
|
|
3
|
+
*
|
|
4
|
+
* Simulates realistic energy grid pricing based on:
|
|
5
|
+
* - Real weather data (temperature, wind, solar radiation)
|
|
6
|
+
* - Regional base pricing
|
|
7
|
+
* - Time-of-day patterns
|
|
8
|
+
* - Seasonal adjustments
|
|
9
|
+
* - Local grid characteristics
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Regional Base Pricing Map (USD/kWh)
|
|
14
|
+
* Based on real-world average electricity prices
|
|
15
|
+
*/
|
|
16
|
+
const REGIONAL_BASE_PRICES = {
|
|
17
|
+
// North America
|
|
18
|
+
US: { base: 0.14, currency: 'USD', name: 'United States' },
|
|
19
|
+
CA: { base: 0.12, currency: 'CAD', name: 'Canada' },
|
|
20
|
+
MX: { base: 0.09, currency: 'MXN', name: 'Mexico' },
|
|
21
|
+
|
|
22
|
+
// Europe
|
|
23
|
+
DE: { base: 0.32, currency: 'EUR', name: 'Germany' },
|
|
24
|
+
FR: { base: 0.19, currency: 'EUR', name: 'France' },
|
|
25
|
+
GB: { base: 0.28, currency: 'GBP', name: 'United Kingdom' },
|
|
26
|
+
ES: { base: 0.24, currency: 'EUR', name: 'Spain' },
|
|
27
|
+
IT: { base: 0.26, currency: 'EUR', name: 'Italy' },
|
|
28
|
+
PL: { base: 0.16, currency: 'EUR', name: 'Poland' },
|
|
29
|
+
NL: { base: 0.27, currency: 'EUR', name: 'Netherlands' },
|
|
30
|
+
|
|
31
|
+
// Asia
|
|
32
|
+
IN: { base: 0.08, currency: 'INR', name: 'India' },
|
|
33
|
+
CN: { base: 0.08, currency: 'CNY', name: 'China' },
|
|
34
|
+
JP: { base: 0.26, currency: 'JPY', name: 'Japan' },
|
|
35
|
+
KR: { base: 0.10, currency: 'KRW', name: 'South Korea' },
|
|
36
|
+
SG: { base: 0.19, currency: 'SGD', name: 'Singapore' },
|
|
37
|
+
|
|
38
|
+
// South America
|
|
39
|
+
BR: { base: 0.11, currency: 'BRL', name: 'Brazil' },
|
|
40
|
+
AR: { base: 0.06, currency: 'ARS', name: 'Argentina' },
|
|
41
|
+
CL: { base: 0.14, currency: 'CLP', name: 'Chile' },
|
|
42
|
+
|
|
43
|
+
// Oceania
|
|
44
|
+
AU: { base: 0.21, currency: 'AUD', name: 'Australia' },
|
|
45
|
+
NZ: { base: 0.18, currency: 'NZD', name: 'New Zealand' },
|
|
46
|
+
|
|
47
|
+
// Africa
|
|
48
|
+
ZA: { base: 0.09, currency: 'ZAR', name: 'South Africa' },
|
|
49
|
+
NG: { base: 0.05, currency: 'NGN', name: 'Nigeria' },
|
|
50
|
+
KE: { base: 0.12, currency: 'KES', name: 'Kenya' },
|
|
51
|
+
|
|
52
|
+
// Default
|
|
53
|
+
DEFAULT: { base: 0.12, currency: 'USD', name: 'Global Average' }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Regional Peak Hour Patterns
|
|
58
|
+
* Different regions have different typical peak hours
|
|
59
|
+
*/
|
|
60
|
+
const REGIONAL_PEAK_PATTERNS = {
|
|
61
|
+
US: { morning: [7, 9], evening: [17, 21], offPeak: [22, 6] },
|
|
62
|
+
EU: { morning: [7, 9], evening: [18, 21], offPeak: [23, 6] },
|
|
63
|
+
IN: { morning: [6, 9], evening: [19, 23], offPeak: [0, 5] }, // India peaks later due to heat
|
|
64
|
+
BR: { morning: [7, 10], evening: [18, 22], offPeak: [23, 6] },
|
|
65
|
+
JP: { morning: [8, 10], evening: [18, 20], offPeak: [22, 6] },
|
|
66
|
+
DEFAULT: { morning: [7, 9], evening: [17, 21], offPeak: [22, 6] }
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get regional pricing info from country code
|
|
71
|
+
*/
|
|
72
|
+
export function getRegionalPricing(countryCode) {
|
|
73
|
+
const code = countryCode ? countryCode.toUpperCase() : 'DEFAULT';
|
|
74
|
+
return REGIONAL_BASE_PRICES[code] || REGIONAL_BASE_PRICES.DEFAULT;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get regional peak pattern
|
|
79
|
+
*/
|
|
80
|
+
function getRegionalPeakPattern(countryCode) {
|
|
81
|
+
// Map country code to region
|
|
82
|
+
const regionMap = {
|
|
83
|
+
US: 'US', CA: 'US', MX: 'US',
|
|
84
|
+
DE: 'EU', FR: 'EU', GB: 'EU', ES: 'EU', IT: 'EU', PL: 'EU', NL: 'EU',
|
|
85
|
+
IN: 'IN',
|
|
86
|
+
BR: 'BR', AR: 'BR', CL: 'BR',
|
|
87
|
+
JP: 'JP', KR: 'JP', CN: 'JP',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const region = regionMap[countryCode] || 'DEFAULT';
|
|
91
|
+
return REGIONAL_PEAK_PATTERNS[region] || REGIONAL_PEAK_PATTERNS.DEFAULT;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if hour is in peak period
|
|
96
|
+
*/
|
|
97
|
+
function isPeakHour(hour, pattern) {
|
|
98
|
+
const morningPeak = hour >= pattern.morning[0] && hour <= pattern.morning[1];
|
|
99
|
+
const eveningPeak = hour >= pattern.evening[0] && hour <= pattern.evening[1];
|
|
100
|
+
return morningPeak || eveningPeak;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if hour is off-peak
|
|
105
|
+
*/
|
|
106
|
+
function isOffPeakHour(hour, pattern) {
|
|
107
|
+
if (pattern.offPeak[0] > pattern.offPeak[1]) {
|
|
108
|
+
// Wraps around midnight (e.g., 22-6)
|
|
109
|
+
return hour >= pattern.offPeak[0] || hour <= pattern.offPeak[1];
|
|
110
|
+
}
|
|
111
|
+
return hour >= pattern.offPeak[0] && hour <= pattern.offPeak[1];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Calculate price multiplier based on weather conditions
|
|
116
|
+
*/
|
|
117
|
+
function calculateWeatherMultiplier(weather) {
|
|
118
|
+
let multiplier = 1.0;
|
|
119
|
+
const reasons = [];
|
|
120
|
+
|
|
121
|
+
// Temperature impact (AC/heating demand)
|
|
122
|
+
if (weather.temperature > 28) {
|
|
123
|
+
const tempImpact = 1 + ((weather.temperature - 28) * 0.03); // +3% per degree above 28°C
|
|
124
|
+
multiplier *= tempImpact;
|
|
125
|
+
reasons.push(`🌡️ High temp (${weather.temperature.toFixed(1)}°C) +${((tempImpact - 1) * 100).toFixed(0)}% AC demand`);
|
|
126
|
+
} else if (weather.temperature < 5) {
|
|
127
|
+
const tempImpact = 1 + ((5 - weather.temperature) * 0.02); // +2% per degree below 5°C
|
|
128
|
+
multiplier *= tempImpact;
|
|
129
|
+
reasons.push(`❄️ Low temp (${weather.temperature.toFixed(1)}°C) +${((tempImpact - 1) * 100).toFixed(0)}% heating demand`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Wind impact (wind energy generation)
|
|
133
|
+
if (weather.windSpeed > 8) {
|
|
134
|
+
const windImpact = 0.75; // -25% when windy
|
|
135
|
+
multiplier *= windImpact;
|
|
136
|
+
reasons.push(`💨 High wind (${weather.windSpeed.toFixed(1)} m/s) -25% renewable energy`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Solar radiation impact (solar energy generation)
|
|
140
|
+
// Only during daytime hours (6-18)
|
|
141
|
+
if (weather.hour >= 6 && weather.hour <= 18 && weather.solarRadiation > 200) {
|
|
142
|
+
const solarImpact = Math.max(0.8, 1 - (weather.solarRadiation / 1000) * 0.2); // Up to -20%
|
|
143
|
+
multiplier *= solarImpact;
|
|
144
|
+
reasons.push(`☀️ High solar (${weather.solarRadiation.toFixed(0)} W/m²) -${((1 - solarImpact) * 100).toFixed(0)}% solar energy`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { multiplier, reasons };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Calculate time-of-day multiplier
|
|
152
|
+
*/
|
|
153
|
+
function calculateTimeMultiplier(hour, pattern) {
|
|
154
|
+
const reasons = [];
|
|
155
|
+
let multiplier = 1.0;
|
|
156
|
+
|
|
157
|
+
if (isPeakHour(hour, pattern)) {
|
|
158
|
+
multiplier = 1.4; // +40% during peak
|
|
159
|
+
const peakType = hour >= pattern.morning[0] && hour <= pattern.morning[1] ? 'morning' : 'evening';
|
|
160
|
+
reasons.push(`⚡ ${peakType.charAt(0).toUpperCase() + peakType.slice(1)} peak hours +40%`);
|
|
161
|
+
} else if (isOffPeakHour(hour, pattern)) {
|
|
162
|
+
multiplier = 0.7; // -30% during off-peak
|
|
163
|
+
reasons.push(`🌙 Off-peak hours -30%`);
|
|
164
|
+
} else {
|
|
165
|
+
reasons.push(`📊 Standard hours`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { multiplier, reasons };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generate smart pricing data based on weather forecast
|
|
173
|
+
*/
|
|
174
|
+
export function generateSmartPricing(weatherData, countryCode = 'DEFAULT') {
|
|
175
|
+
const regionalPricing = getRegionalPricing(countryCode);
|
|
176
|
+
const peakPattern = getRegionalPeakPattern(countryCode);
|
|
177
|
+
const basePrice = regionalPricing.base;
|
|
178
|
+
|
|
179
|
+
console.log(`💰 Base price: $${basePrice.toFixed(4)}/kWh (${regionalPricing.name})`);
|
|
180
|
+
|
|
181
|
+
const pricingData = [];
|
|
182
|
+
|
|
183
|
+
for (const weather of weatherData.forecast) {
|
|
184
|
+
// Calculate multipliers
|
|
185
|
+
const weatherMultiplier = calculateWeatherMultiplier(weather);
|
|
186
|
+
const timeMultiplier = calculateTimeMultiplier(weather.hour, peakPattern);
|
|
187
|
+
|
|
188
|
+
// Calculate final price
|
|
189
|
+
const finalMultiplier = weatherMultiplier.multiplier * timeMultiplier.multiplier;
|
|
190
|
+
const price = basePrice * finalMultiplier;
|
|
191
|
+
|
|
192
|
+
// Calculate simulated demand (inversely related to price for renewable-heavy grids)
|
|
193
|
+
const baseDemand = 40000;
|
|
194
|
+
const demandMultiplier = 1 + (finalMultiplier - 1) * 0.5; // Demand follows price but not as strongly
|
|
195
|
+
const demand = baseDemand * demandMultiplier;
|
|
196
|
+
|
|
197
|
+
pricingData.push({
|
|
198
|
+
timestamp: weather.timestamp,
|
|
199
|
+
hour: weather.hour,
|
|
200
|
+
price: Math.max(basePrice * 0.5, Math.min(basePrice * 2.0, price)), // Cap at 50%-200% of base
|
|
201
|
+
demand,
|
|
202
|
+
weather: {
|
|
203
|
+
temperature: weather.temperature,
|
|
204
|
+
windSpeed: weather.windSpeed,
|
|
205
|
+
solarRadiation: weather.solarRadiation,
|
|
206
|
+
humidity: weather.humidity
|
|
207
|
+
},
|
|
208
|
+
reasons: [...timeMultiplier.reasons, ...weatherMultiplier.reasons],
|
|
209
|
+
source: 'Smart-Simulation-Weather'
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return pricingData;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get insights about pricing patterns
|
|
218
|
+
*/
|
|
219
|
+
export function getPricingInsights(pricingData, countryCode) {
|
|
220
|
+
const prices = pricingData.map(d => d.price);
|
|
221
|
+
const minPrice = Math.min(...prices);
|
|
222
|
+
const maxPrice = Math.max(...prices);
|
|
223
|
+
const avgPrice = prices.reduce((a, b) => a + b, 0) / prices.length;
|
|
224
|
+
|
|
225
|
+
const cheapestHour = pricingData.find(d => d.price === minPrice);
|
|
226
|
+
const expensiveHour = pricingData.find(d => d.price === maxPrice);
|
|
227
|
+
|
|
228
|
+
const regionalPricing = getRegionalPricing(countryCode);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
region: regionalPricing.name,
|
|
232
|
+
baseCurrency: regionalPricing.currency,
|
|
233
|
+
avgPrice: avgPrice.toFixed(4),
|
|
234
|
+
minPrice: minPrice.toFixed(4),
|
|
235
|
+
maxPrice: maxPrice.toFixed(4),
|
|
236
|
+
priceRange: ((maxPrice - minPrice) / avgPrice * 100).toFixed(1),
|
|
237
|
+
cheapestTime: {
|
|
238
|
+
hour: cheapestHour.hour,
|
|
239
|
+
timestamp: cheapestHour.timestamp,
|
|
240
|
+
price: cheapestHour.price.toFixed(4),
|
|
241
|
+
reasons: cheapestHour.reasons
|
|
242
|
+
},
|
|
243
|
+
expensiveTime: {
|
|
244
|
+
hour: expensiveHour.hour,
|
|
245
|
+
timestamp: expensiveHour.timestamp,
|
|
246
|
+
price: expensiveHour.price.toFixed(4),
|
|
247
|
+
reasons: expensiveHour.reasons
|
|
248
|
+
},
|
|
249
|
+
savingsPotential: (((maxPrice - minPrice) / maxPrice) * 100).toFixed(1)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weather Data Module - Open-Meteo API Integration
|
|
3
|
+
*
|
|
4
|
+
* Fetches real-time weather forecasts to simulate energy grid pricing.
|
|
5
|
+
* Uses FREE Open-Meteo API (no key needed!)
|
|
6
|
+
*
|
|
7
|
+
* API Docs: https://open-meteo.com/en/docs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fetch from 'node-fetch';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get coordinates from location string
|
|
14
|
+
* Simple geocoding using Open-Meteo's geocoding API (also free!)
|
|
15
|
+
*/
|
|
16
|
+
export async function getCoordinatesFromLocation(location) {
|
|
17
|
+
try {
|
|
18
|
+
const locationEncoded = encodeURIComponent(location);
|
|
19
|
+
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${locationEncoded}&count=1&language=en&format=json`;
|
|
20
|
+
|
|
21
|
+
const response = await fetch(url);
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`Geocoding failed: ${response.status}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
|
|
28
|
+
if (!data.results || data.results.length === 0) {
|
|
29
|
+
throw new Error('Location not found');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result = data.results[0];
|
|
33
|
+
return {
|
|
34
|
+
latitude: result.latitude,
|
|
35
|
+
longitude: result.longitude,
|
|
36
|
+
name: result.name,
|
|
37
|
+
country: result.country,
|
|
38
|
+
timezone: result.timezone || 'UTC'
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(`Failed to geocode location: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fetch weather forecast from Open-Meteo API (FREE!)
|
|
47
|
+
* Returns 24-hour forecast with temperature, wind, humidity, and solar radiation
|
|
48
|
+
*/
|
|
49
|
+
export async function fetchWeatherForecast(latitude, longitude, timezone = 'auto') {
|
|
50
|
+
try {
|
|
51
|
+
const url = `https://api.open-meteo.com/v1/forecast?` +
|
|
52
|
+
`latitude=${latitude}` +
|
|
53
|
+
`&longitude=${longitude}` +
|
|
54
|
+
`&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m,shortwave_radiation` +
|
|
55
|
+
`&timezone=${timezone}` +
|
|
56
|
+
`&forecast_days=2`; // Get 2 days for full 24h coverage
|
|
57
|
+
|
|
58
|
+
console.log(`🌤️ Fetching weather from Open-Meteo API (FREE!)...`);
|
|
59
|
+
|
|
60
|
+
const response = await fetch(url);
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Open-Meteo API error: ${response.status}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
|
|
68
|
+
if (!data.hourly) {
|
|
69
|
+
throw new Error('No hourly forecast data available');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Parse hourly data
|
|
73
|
+
const hourlyData = [];
|
|
74
|
+
const currentHour = new Date().getHours();
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < Math.min(48, data.hourly.time.length); i++) {
|
|
77
|
+
const timestamp = data.hourly.time[i];
|
|
78
|
+
const hour = new Date(timestamp).getHours();
|
|
79
|
+
|
|
80
|
+
hourlyData.push({
|
|
81
|
+
timestamp,
|
|
82
|
+
hour,
|
|
83
|
+
temperature: data.hourly.temperature_2m[i] || 20, // Celsius
|
|
84
|
+
humidity: data.hourly.relative_humidity_2m[i] || 50, // %
|
|
85
|
+
windSpeed: data.hourly.wind_speed_10m[i] || 0, // m/s
|
|
86
|
+
solarRadiation: data.hourly.shortwave_radiation[i] || 0, // W/m²
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(`✓ Fetched ${hourlyData.length} hours of weather forecast`);
|
|
91
|
+
return hourlyData;
|
|
92
|
+
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw new Error(`Failed to fetch weather data: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get weather data for a location
|
|
100
|
+
*/
|
|
101
|
+
export async function getWeatherForLocation(location) {
|
|
102
|
+
try {
|
|
103
|
+
// Get coordinates
|
|
104
|
+
const coords = await getCoordinatesFromLocation(location);
|
|
105
|
+
console.log(`📍 Location: ${coords.name}, ${coords.country} (${coords.latitude}, ${coords.longitude})`);
|
|
106
|
+
|
|
107
|
+
// Get weather forecast
|
|
108
|
+
const forecast = await fetchWeatherForecast(coords.latitude, coords.longitude, coords.timezone);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
location: coords,
|
|
112
|
+
forecast
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new Error(`Failed to get weather for location: ${error.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|