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.
@@ -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
+ }