decharge-scout 4.3.2 β 4.6.0
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.example +4 -0
- package/README.md +53 -0
- package/index.js +103 -1
- package/package.json +1 -1
- package/src/fleet.js +449 -0
- package/src/oracle.js +20 -1
- package/src/weather-data.js +28 -0
package/.env.example
CHANGED
|
@@ -28,6 +28,10 @@ STAKE_AMOUNT=0.01
|
|
|
28
28
|
# Premium Feature Price (in SOL)
|
|
29
29
|
PREMIUM_PRICE=0.001
|
|
30
30
|
|
|
31
|
+
# Fleet Treasury Address (for fleet creation fees)
|
|
32
|
+
# Replace with your devnet treasury wallet for testing
|
|
33
|
+
FLEET_TREASURY_ADDRESS=FLEETTreasuryPubkey11111111111111111111111111
|
|
34
|
+
|
|
31
35
|
# Query Cycle Interval (in minutes)
|
|
32
36
|
# How often to fetch weather & submit data (default: 15)
|
|
33
37
|
# For testing, you can set to 2 minutes: CYCLE_INTERVAL_MINUTES=2
|
package/README.md
CHANGED
|
@@ -34,6 +34,14 @@ Then run: `decharge-scout`
|
|
|
34
34
|
- Calculates potential savings (%)
|
|
35
35
|
- Regional peak/off-peak pattern matching
|
|
36
36
|
|
|
37
|
+
### π **Virtual Fleet Optimizer** (NEW!)
|
|
38
|
+
- Optimize EV fleet charging across real driving routes
|
|
39
|
+
- FREE geocoding (Nominatim) and routing (OSRM)
|
|
40
|
+
- Identifies optimal charging stops every ~300km
|
|
41
|
+
- Multi-vehicle cost analysis with weather-based simulation
|
|
42
|
+
- COβ savings calculation
|
|
43
|
+
- Earns 5-10 points per fleet optimization
|
|
44
|
+
|
|
37
45
|
### π **Local Alpha Contribution**
|
|
38
46
|
- Share your local grid knowledge (e.g., "7-9PM peak in Lagos")
|
|
39
47
|
- Earn bonus points for contributing
|
|
@@ -133,6 +141,39 @@ The CLI will automatically:
|
|
|
133
141
|
- Prompt for EIA API key if missing
|
|
134
142
|
- Auto-detect your location
|
|
135
143
|
|
|
144
|
+
### Fleet Optimizer (NEW!)
|
|
145
|
+
|
|
146
|
+
Optimize EV fleet charging across real routes:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
decharge-scout fleet --from="New York" --to="Boston" --evs=10 --agent-name="MyFleetBot"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**What it does:**
|
|
153
|
+
- Geocodes your start/end cities (FREE Nominatim API)
|
|
154
|
+
- Calculates real driving route (FREE OSRM API)
|
|
155
|
+
- Analyzes weather along the route for optimal charging windows
|
|
156
|
+
- Identifies charging stops every ~300km at lowest-price times
|
|
157
|
+
- Calculates total cost, savings %, and COβ impact
|
|
158
|
+
- Submits to Solana blockchain and AgentOne dashboard
|
|
159
|
+
|
|
160
|
+
**Cost:** 0.005 SOL one-time fleet creation fee
|
|
161
|
+
**Rewards:** 5-10 points per optimization (bonus for >25% savings)
|
|
162
|
+
|
|
163
|
+
**Examples:**
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# Single vehicle, short route
|
|
167
|
+
decharge-scout fleet --from="San Francisco" --to="Los Angeles" --evs=1
|
|
168
|
+
|
|
169
|
+
# Large fleet, long route
|
|
170
|
+
decharge-scout fleet --from="New York" --to="Miami" --evs=50 --agent-name="EastCoastFleet"
|
|
171
|
+
|
|
172
|
+
# International routes
|
|
173
|
+
decharge-scout fleet --from="Berlin" --to="Paris" --evs=20
|
|
174
|
+
decharge-scout fleet --from="Delhi" --to="Mumbai" --evs=15
|
|
175
|
+
```
|
|
176
|
+
|
|
136
177
|
### With Custom Options
|
|
137
178
|
|
|
138
179
|
```bash
|
|
@@ -156,6 +197,9 @@ decharge-scout --premium
|
|
|
156
197
|
```bash
|
|
157
198
|
decharge-scout --help
|
|
158
199
|
|
|
200
|
+
Commands:
|
|
201
|
+
fleet [options] Optimize EV fleet charging across a route
|
|
202
|
+
|
|
159
203
|
Options:
|
|
160
204
|
-w, --wallet <path> Path to Solana wallet JSON keypair file (default: ./wallet.json)
|
|
161
205
|
-a, --agent-name <name> Custom agent name (default: auto-generated)
|
|
@@ -163,6 +207,12 @@ Options:
|
|
|
163
207
|
-p, --premium Enable premium features (x402 micropayments)
|
|
164
208
|
-h, --help Display help for command
|
|
165
209
|
-V, --version Output the version number
|
|
210
|
+
|
|
211
|
+
Fleet Options:
|
|
212
|
+
--from <city> Starting city (required)
|
|
213
|
+
--to <city> Destination city (required)
|
|
214
|
+
--evs <number> Number of electric vehicles (default: 1)
|
|
215
|
+
-a, --agent-name <name> Custom agent name (default: auto-generated)
|
|
166
216
|
```
|
|
167
217
|
|
|
168
218
|
## How It Works
|
|
@@ -255,6 +305,9 @@ decharge-scout/
|
|
|
255
305
|
β βββ oracle.js # Solana oracle submission
|
|
256
306
|
β βββ points.js # Points tracking system
|
|
257
307
|
β βββ geolocation.js # IP-based location detection
|
|
308
|
+
β βββ fleet.js # Fleet optimization module (NEW!)
|
|
309
|
+
β βββ weather-data.js # Weather forecasting
|
|
310
|
+
β βββ smart-pricing.js # Pricing simulation engine
|
|
258
311
|
β βββ x402.js # x402 micropayment handling
|
|
259
312
|
```
|
|
260
313
|
|
package/index.js
CHANGED
|
@@ -41,6 +41,7 @@ import { initializePoints, awardPoints, getPoints, savePoints } from './src/poin
|
|
|
41
41
|
import { getLocation } from './src/geolocation.js';
|
|
42
42
|
import { purchasePremiumData } from './src/x402.js';
|
|
43
43
|
import { parseAlphaContribution, saveAlphaContribution, calculateAlphaBonus, getAlphaInsights, verifyContribution, getInformationSources } from './src/local-alpha.js';
|
|
44
|
+
import { runFleetOptimization } from './src/fleet.js';
|
|
44
45
|
|
|
45
46
|
const __filename = fileURLToPath(import.meta.url);
|
|
46
47
|
const __dirname = dirname(__filename);
|
|
@@ -618,17 +619,118 @@ function sleep(ms) {
|
|
|
618
619
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
619
620
|
}
|
|
620
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Fleet optimization command handler
|
|
624
|
+
*/
|
|
625
|
+
async function fleetCommand(options) {
|
|
626
|
+
console.log(chalk.cyan.bold('\nπ Virtual Fleet Optimizer\n'));
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
// Auto-configure environment
|
|
630
|
+
await ensureEnvironment();
|
|
631
|
+
|
|
632
|
+
// Start wallet server
|
|
633
|
+
console.log(chalk.blue('π Starting browser wallet connection...'));
|
|
634
|
+
const server = await startWalletServer();
|
|
635
|
+
|
|
636
|
+
if (!server) {
|
|
637
|
+
console.log(chalk.red('β Failed to start wallet server'));
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Open browser for wallet connection
|
|
642
|
+
await openWalletConnection();
|
|
643
|
+
|
|
644
|
+
// Wait for wallet connection
|
|
645
|
+
const walletSpinner = ora('Waiting for wallet connection in browser...').start();
|
|
646
|
+
const walletAddress = await waitForWalletConnection();
|
|
647
|
+
walletSpinner.succeed(chalk.green(`β Wallet connected: ${walletAddress}`));
|
|
648
|
+
|
|
649
|
+
// Set connected wallet
|
|
650
|
+
setConnectedWallet(walletAddress);
|
|
651
|
+
|
|
652
|
+
// Check wallet balance
|
|
653
|
+
const balanceSpinner = ora('Checking wallet balance...').start();
|
|
654
|
+
const balance = await getBalance();
|
|
655
|
+
balanceSpinner.succeed(chalk.green(`π° Wallet Balance: ${balance.toFixed(4)} SOL`));
|
|
656
|
+
|
|
657
|
+
// Verify sufficient balance for fleet creation fee (0.005 SOL + fees)
|
|
658
|
+
const FLEET_FEE = 0.005;
|
|
659
|
+
try {
|
|
660
|
+
await checkBalance(FLEET_FEE + 0.001);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
console.log(chalk.red(`\nβ ${error.message}`));
|
|
663
|
+
console.log(chalk.yellow(`\nFleet creation requires ${FLEET_FEE} SOL + fees`));
|
|
664
|
+
console.log(chalk.yellow(`Please fund your wallet and try again:`));
|
|
665
|
+
console.log(chalk.blue(`https://faucet.solana.com/`));
|
|
666
|
+
console.log(chalk.gray(`Wallet: ${walletAddress}\n`));
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Set agent name (use provided or generate)
|
|
671
|
+
const agentName = options.agentName || generateAgentName();
|
|
672
|
+
|
|
673
|
+
// Initialize points system
|
|
674
|
+
initializePoints(walletAddress);
|
|
675
|
+
|
|
676
|
+
// Validate required options
|
|
677
|
+
if (!options.from || !options.to) {
|
|
678
|
+
console.log(chalk.red('\nβ Missing required parameters: --from and --to'));
|
|
679
|
+
console.log(chalk.yellow('\nUsage:'));
|
|
680
|
+
console.log(chalk.gray(' decharge-scout fleet --from="New York" --to="Boston" --evs=10 --agent-name="MyFleet"'));
|
|
681
|
+
console.log(chalk.gray('\nExample:'));
|
|
682
|
+
console.log(chalk.gray(' decharge-scout fleet --from="San Francisco" --to="Los Angeles" --evs=5\n'));
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Default EVs to 1 if not provided
|
|
687
|
+
const evs = parseInt(options.evs) || 1;
|
|
688
|
+
|
|
689
|
+
if (evs < 1 || evs > 1000) {
|
|
690
|
+
console.log(chalk.red('\nβ Invalid number of EVs. Must be between 1 and 1000\n'));
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Run fleet optimization
|
|
695
|
+
await runFleetOptimization({
|
|
696
|
+
agentName,
|
|
697
|
+
from: options.from,
|
|
698
|
+
to: options.to,
|
|
699
|
+
evs
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
rl.close();
|
|
703
|
+
process.exit(0);
|
|
704
|
+
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.error(chalk.red(`\nβ Error: ${error.message}`));
|
|
707
|
+
console.error(chalk.gray(error.stack));
|
|
708
|
+
rl.close();
|
|
709
|
+
process.exit(1);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
621
713
|
// CLI Setup
|
|
622
714
|
const program = new Command();
|
|
623
715
|
|
|
624
716
|
program
|
|
625
717
|
.name('decharge-scout')
|
|
626
718
|
.description('AI-powered energy grid data scout with Solana integration')
|
|
627
|
-
.version('
|
|
719
|
+
.version('0.3.0')
|
|
628
720
|
.option('-w, --wallet <path>', 'Path to Solana wallet JSON keypair file (default: ./wallet.json)')
|
|
629
721
|
.option('-a, --agent-name <name>', 'Custom agent name (default: auto-generated)')
|
|
630
722
|
.option('-l, --location <location>', 'Manual location override (default: auto-detect via IP)')
|
|
631
723
|
.option('-p, --premium', 'Enable premium features (x402 micropayments)')
|
|
632
724
|
.action(main);
|
|
633
725
|
|
|
726
|
+
// Add fleet subcommand
|
|
727
|
+
program
|
|
728
|
+
.command('fleet')
|
|
729
|
+
.description('Optimize EV fleet charging across a route')
|
|
730
|
+
.requiredOption('--from <city>', 'Starting city (e.g., "New York")')
|
|
731
|
+
.requiredOption('--to <city>', 'Destination city (e.g., "Boston")')
|
|
732
|
+
.option('--evs <number>', 'Number of electric vehicles in fleet', '1')
|
|
733
|
+
.option('-a, --agent-name <name>', 'Custom agent name (default: auto-generated)')
|
|
734
|
+
.action(fleetCommand);
|
|
735
|
+
|
|
634
736
|
program.parse();
|
package/package.json
CHANGED
package/src/fleet.js
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual Fleet Optimizer Module
|
|
3
|
+
*
|
|
4
|
+
* Optimizes EV fleet charging across routes using:
|
|
5
|
+
* - Free geocoding (Nominatim)
|
|
6
|
+
* - Free routing (OSRM)
|
|
7
|
+
* - Weather-based energy simulation (existing engine)
|
|
8
|
+
* - Solana blockchain integration
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fetch from 'node-fetch';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import ora from 'ora';
|
|
14
|
+
import { PublicKey, Transaction, SystemProgram, LAMPORTS_PER_SOL, sendAndConfirmTransaction } from '@solana/web3.js';
|
|
15
|
+
import { getConnection } from './wallet.js';
|
|
16
|
+
import { getConnectedWallet, setConnectedWallet, checkBalance } from './browser-wallet.js';
|
|
17
|
+
import { getWeatherForLocation } from './weather-data.js';
|
|
18
|
+
import { generateSmartPricing } from './smart-pricing.js';
|
|
19
|
+
import { findCheapestWindow } from './optimizer.js';
|
|
20
|
+
import { submitToOracle } from './oracle.js';
|
|
21
|
+
import { awardPoints, getPoints, savePoints } from './points.js';
|
|
22
|
+
import dotenv from 'dotenv';
|
|
23
|
+
|
|
24
|
+
dotenv.config();
|
|
25
|
+
|
|
26
|
+
// Fleet creation fee
|
|
27
|
+
const FLEET_CREATION_FEE = 0.005; // 0.005 SOL per fleet creation
|
|
28
|
+
|
|
29
|
+
// Treasury address for fleet fees (devnet)
|
|
30
|
+
const FLEET_TREASURY_PUBKEY = process.env.FLEET_TREASURY_ADDRESS ||
|
|
31
|
+
'FLEETTreasuryPubkey11111111111111111111111111';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Geocode a city name to coordinates using Nominatim (OpenStreetMap)
|
|
35
|
+
* Free, no API key needed, works globally
|
|
36
|
+
*/
|
|
37
|
+
async function geocodeCity(cityName) {
|
|
38
|
+
try {
|
|
39
|
+
const encodedCity = encodeURIComponent(cityName);
|
|
40
|
+
const url = `https://nominatim.openstreetmap.org/search?q=${encodedCity}&format=json&limit=1`;
|
|
41
|
+
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
headers: {
|
|
44
|
+
'User-Agent': 'decharge-scout-fleet/1.0'
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Geocoding failed: ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const data = await response.json();
|
|
53
|
+
|
|
54
|
+
if (!data || data.length === 0) {
|
|
55
|
+
throw new Error(`City "${cityName}" not found`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = data[0];
|
|
59
|
+
return {
|
|
60
|
+
city: result.display_name.split(',')[0],
|
|
61
|
+
lat: parseFloat(result.lat),
|
|
62
|
+
lon: parseFloat(result.lon),
|
|
63
|
+
display_name: result.display_name
|
|
64
|
+
};
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(`Geocoding error: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get driving route using OSRM (Open Source Routing Machine)
|
|
72
|
+
* Free, no API key needed
|
|
73
|
+
*/
|
|
74
|
+
async function getRoute(fromCoords, toCoords) {
|
|
75
|
+
try {
|
|
76
|
+
const url = `https://router.project-osrm.org/route/v1/driving/${fromCoords.lon},${fromCoords.lat};${toCoords.lon},${toCoords.lat}?overview=full&geometries=geojson`;
|
|
77
|
+
|
|
78
|
+
const response = await fetch(url);
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`Routing failed: ${response.status}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
|
|
86
|
+
if (!data.routes || data.routes.length === 0) {
|
|
87
|
+
throw new Error('No route found between cities');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const route = data.routes[0];
|
|
91
|
+
return {
|
|
92
|
+
geometry: route.geometry, // GeoJSON LineString
|
|
93
|
+
distance: route.distance / 1000, // Convert meters to km
|
|
94
|
+
duration: route.duration / 60 // Convert seconds to minutes
|
|
95
|
+
};
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new Error(`Routing error: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Divide route into segments for charging analysis
|
|
103
|
+
*/
|
|
104
|
+
function createRouteSegments(route, segmentDistanceKm = 75) {
|
|
105
|
+
const coords = route.geometry.coordinates;
|
|
106
|
+
const totalDistance = route.distance;
|
|
107
|
+
const numSegments = Math.ceil(totalDistance / segmentDistanceKm);
|
|
108
|
+
|
|
109
|
+
const segments = [];
|
|
110
|
+
const coordsPerSegment = Math.max(1, Math.floor(coords.length / numSegments));
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < numSegments; i++) {
|
|
113
|
+
const startIdx = i * coordsPerSegment;
|
|
114
|
+
const endIdx = Math.min((i + 1) * coordsPerSegment, coords.length - 1);
|
|
115
|
+
|
|
116
|
+
// Get midpoint of segment
|
|
117
|
+
const midIdx = Math.floor((startIdx + endIdx) / 2);
|
|
118
|
+
const midCoord = coords[midIdx];
|
|
119
|
+
|
|
120
|
+
segments.push({
|
|
121
|
+
segmentId: i + 1,
|
|
122
|
+
startIdx,
|
|
123
|
+
endIdx,
|
|
124
|
+
midpoint: {
|
|
125
|
+
lon: midCoord[0],
|
|
126
|
+
lat: midCoord[1]
|
|
127
|
+
},
|
|
128
|
+
distanceFromStart: (totalDistance / numSegments) * i
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return segments;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Simulate charging costs for each segment
|
|
137
|
+
*/
|
|
138
|
+
async function simulateSegmentCharging(segment, fleetSize) {
|
|
139
|
+
try {
|
|
140
|
+
// Create a location string from coordinates
|
|
141
|
+
const location = `${segment.midpoint.lat.toFixed(2)},${segment.midpoint.lon.toFixed(2)}`;
|
|
142
|
+
|
|
143
|
+
// Get weather forecast for this location
|
|
144
|
+
const weatherData = await getWeatherForLocation(location);
|
|
145
|
+
const countryCode = weatherData.location.country || 'US';
|
|
146
|
+
|
|
147
|
+
// Generate pricing simulation
|
|
148
|
+
const energyData = generateSmartPricing(weatherData, countryCode);
|
|
149
|
+
|
|
150
|
+
// Find cheapest charging window
|
|
151
|
+
const cheapestWindow = findCheapestWindow(energyData);
|
|
152
|
+
|
|
153
|
+
// Calculate cost for fleet (assume 60 kWh per vehicle)
|
|
154
|
+
const kWhPerVehicle = 60;
|
|
155
|
+
const totalKwh = kWhPerVehicle * fleetSize;
|
|
156
|
+
const chargingCost = totalKwh * cheapestWindow.price;
|
|
157
|
+
|
|
158
|
+
// Calculate what random charging would cost (use average price)
|
|
159
|
+
const avgPrice = energyData.reduce((sum, e) => sum + e.price, 0) / energyData.length;
|
|
160
|
+
const randomCost = totalKwh * avgPrice;
|
|
161
|
+
const savings = ((randomCost - chargingCost) / randomCost) * 100;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
segmentId: segment.segmentId,
|
|
165
|
+
location: weatherData.location,
|
|
166
|
+
coordinates: segment.midpoint,
|
|
167
|
+
cheapestWindow: cheapestWindow.timeWindow,
|
|
168
|
+
price: cheapestWindow.price,
|
|
169
|
+
chargingCost: chargingCost,
|
|
170
|
+
savings: savings,
|
|
171
|
+
weather: {
|
|
172
|
+
temperature: weatherData.forecast[cheapestWindow.hour]?.temperature,
|
|
173
|
+
windSpeed: weatherData.forecast[cheapestWindow.hour]?.windSpeed,
|
|
174
|
+
solarRadiation: weatherData.forecast[cheapestWindow.hour]?.solarRadiation
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.warn(`Warning: Could not simulate segment ${segment.segmentId}: ${error.message}`);
|
|
179
|
+
// Return fallback data
|
|
180
|
+
return {
|
|
181
|
+
segmentId: segment.segmentId,
|
|
182
|
+
coordinates: segment.midpoint,
|
|
183
|
+
cheapestWindow: 'N/A',
|
|
184
|
+
price: 0.10,
|
|
185
|
+
chargingCost: 60 * 0.10,
|
|
186
|
+
savings: 0,
|
|
187
|
+
weather: null
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Identify optimal charging stops (every ~300km or best savings window)
|
|
194
|
+
*/
|
|
195
|
+
function identifyChargingStops(segmentResults, stopIntervalKm = 300) {
|
|
196
|
+
const stops = [];
|
|
197
|
+
let lastStopDistance = 0;
|
|
198
|
+
|
|
199
|
+
for (const segment of segmentResults) {
|
|
200
|
+
const shouldStop = (segment.distanceFromStart - lastStopDistance) >= stopIntervalKm;
|
|
201
|
+
|
|
202
|
+
if (shouldStop || stops.length === 0) {
|
|
203
|
+
stops.push({
|
|
204
|
+
lat: segment.coordinates.lat,
|
|
205
|
+
lon: segment.coordinates.lon,
|
|
206
|
+
time: segment.cheapestWindow,
|
|
207
|
+
price: segment.price,
|
|
208
|
+
savings: Math.round(segment.savings),
|
|
209
|
+
location: segment.location?.name || 'Unknown',
|
|
210
|
+
segmentId: segment.segmentId
|
|
211
|
+
});
|
|
212
|
+
lastStopDistance = segment.distanceFromStart;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return stops;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Calculate CO2 savings (simplified model)
|
|
221
|
+
*/
|
|
222
|
+
function calculateCO2Savings(distanceKm, avgSavingsPercent) {
|
|
223
|
+
// Simplified: avg grid carbon intensity ~0.5 kg CO2/kWh
|
|
224
|
+
// EV efficiency ~0.2 kWh/km
|
|
225
|
+
const kWhUsed = distanceKm * 0.2;
|
|
226
|
+
const carbonWithoutOptimization = kWhUsed * 0.5;
|
|
227
|
+
const carbonWithOptimization = carbonWithoutOptimization * (1 - avgSavingsPercent / 100);
|
|
228
|
+
const co2Saved = carbonWithoutOptimization - carbonWithOptimization;
|
|
229
|
+
|
|
230
|
+
return Math.round(co2Saved);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Pay fleet creation fee
|
|
235
|
+
*/
|
|
236
|
+
async function payFleetCreationFee(walletAddress) {
|
|
237
|
+
try {
|
|
238
|
+
const connection = getConnection();
|
|
239
|
+
const publicKey = new PublicKey(walletAddress);
|
|
240
|
+
|
|
241
|
+
// Check balance first
|
|
242
|
+
const balance = await connection.getBalance(publicKey);
|
|
243
|
+
const balanceSOL = balance / LAMPORTS_PER_SOL;
|
|
244
|
+
|
|
245
|
+
if (balanceSOL < FLEET_CREATION_FEE + 0.001) {
|
|
246
|
+
throw new Error(`Insufficient balance: ${balanceSOL.toFixed(4)} SOL. Need at least ${FLEET_CREATION_FEE + 0.001} SOL`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// For browser wallet, we can't sign server-side
|
|
250
|
+
// In production, this would be sent to browser for signing
|
|
251
|
+
console.log(chalk.blue(`\nπ° Fleet creation fee: ${FLEET_CREATION_FEE} SOL`));
|
|
252
|
+
console.log(chalk.gray(` Treasury: ${FLEET_TREASURY_PUBKEY}`));
|
|
253
|
+
console.log(chalk.gray(` Your balance: ${balanceSOL.toFixed(4)} SOL`));
|
|
254
|
+
|
|
255
|
+
// Mock transaction for browser wallet
|
|
256
|
+
console.log(chalk.green(`β Fleet creation fee simulated (browser wallet mode)`));
|
|
257
|
+
return 'MOCK_FLEET_FEE_TX_' + Date.now();
|
|
258
|
+
|
|
259
|
+
} catch (error) {
|
|
260
|
+
throw new Error(`Fleet fee payment failed: ${error.message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Main fleet optimization function
|
|
266
|
+
*/
|
|
267
|
+
export async function runFleetOptimization(options) {
|
|
268
|
+
console.log(chalk.cyan.bold('\nπ Virtual Fleet Optimizer\n'));
|
|
269
|
+
console.log(chalk.gray(' Multi-vehicle route optimization with weather-based charging\n'));
|
|
270
|
+
|
|
271
|
+
const { agentName, from, to, evs } = options;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
// Get connected wallet
|
|
275
|
+
const walletAddress = getConnectedWallet();
|
|
276
|
+
if (!walletAddress) {
|
|
277
|
+
throw new Error('No wallet connected. Please run the main scout first to connect wallet.');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
console.log(chalk.blue(`π€ Agent: ${agentName}`));
|
|
281
|
+
console.log(chalk.blue(`π Route: ${from} β ${to}`));
|
|
282
|
+
console.log(chalk.blue(`π Fleet Size: ${evs} vehicles\n`));
|
|
283
|
+
|
|
284
|
+
// Check balance for fleet creation fee
|
|
285
|
+
console.log(chalk.yellow(`π° Checking balance for fleet creation fee...`));
|
|
286
|
+
await checkBalance(FLEET_CREATION_FEE + 0.001);
|
|
287
|
+
|
|
288
|
+
// Pay fleet creation fee
|
|
289
|
+
const feeSpinner = ora(`Paying ${FLEET_CREATION_FEE} SOL fleet creation fee...`).start();
|
|
290
|
+
const feeTx = await payFleetCreationFee(walletAddress);
|
|
291
|
+
feeSpinner.succeed(chalk.green(`β ${FLEET_CREATION_FEE} SOL paid to create your virtual fleet`));
|
|
292
|
+
console.log(chalk.gray(` TX: ${feeTx}\n`));
|
|
293
|
+
|
|
294
|
+
// Geocode cities
|
|
295
|
+
console.log(chalk.blue('π Geocoding locations...'));
|
|
296
|
+
const geocodeSpinner = ora('Looking up coordinates...').start();
|
|
297
|
+
|
|
298
|
+
const fromCoords = await geocodeCity(from);
|
|
299
|
+
const toCoords = await geocodeCity(to);
|
|
300
|
+
|
|
301
|
+
geocodeSpinner.succeed(chalk.green('β Locations found'));
|
|
302
|
+
console.log(chalk.gray(` From: ${fromCoords.display_name}`));
|
|
303
|
+
console.log(chalk.gray(` To: ${toCoords.display_name}\n`));
|
|
304
|
+
|
|
305
|
+
// Get route
|
|
306
|
+
const routeSpinner = ora('Calculating route...').start();
|
|
307
|
+
const route = await getRoute(fromCoords, toCoords);
|
|
308
|
+
routeSpinner.succeed(chalk.green(`β Route calculated: ${route.distance.toFixed(0)} km, ~${Math.round(route.duration / 60)} hours`));
|
|
309
|
+
|
|
310
|
+
// Create route segments
|
|
311
|
+
const segments = createRouteSegments(route, 75);
|
|
312
|
+
console.log(chalk.gray(` Divided into ${segments.length} segments for analysis\n`));
|
|
313
|
+
|
|
314
|
+
// Simulate charging for each segment
|
|
315
|
+
console.log(chalk.blue('β‘ Simulating optimal charging windows...'));
|
|
316
|
+
const simulationSpinner = ora('Analyzing weather and pricing data...').start();
|
|
317
|
+
|
|
318
|
+
const segmentResults = [];
|
|
319
|
+
for (let i = 0; i < segments.length; i++) {
|
|
320
|
+
const segment = segments[i];
|
|
321
|
+
simulationSpinner.text = `Analyzing segment ${i + 1}/${segments.length}...`;
|
|
322
|
+
|
|
323
|
+
const result = await simulateSegmentCharging(segment, evs);
|
|
324
|
+
result.distanceFromStart = segment.distanceFromStart;
|
|
325
|
+
segmentResults.push(result);
|
|
326
|
+
|
|
327
|
+
// Rate limit: wait a bit between API calls
|
|
328
|
+
if (i < segments.length - 1) {
|
|
329
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
simulationSpinner.succeed(chalk.green(`β Simulated ${segmentResults.length} route segments\n`));
|
|
334
|
+
|
|
335
|
+
// Identify optimal charging stops
|
|
336
|
+
const stops = identifyChargingStops(segmentResults, 300);
|
|
337
|
+
|
|
338
|
+
// Calculate summary statistics
|
|
339
|
+
const totalCost = segmentResults.reduce((sum, s) => sum + s.chargingCost, 0);
|
|
340
|
+
const avgSavings = segmentResults.reduce((sum, s) => sum + s.savings, 0) / segmentResults.length;
|
|
341
|
+
const co2Saved = calculateCO2Savings(route.distance, avgSavings);
|
|
342
|
+
|
|
343
|
+
// Display results
|
|
344
|
+
console.log(chalk.green.bold('β¨ Fleet Optimization Results:\n'));
|
|
345
|
+
console.log(chalk.white(`π Total Distance: ${route.distance.toFixed(0)} km`));
|
|
346
|
+
console.log(chalk.white(`β±οΈ Estimated Duration: ${Math.round(route.duration / 60)} hours`));
|
|
347
|
+
console.log(chalk.white(`π Fleet Size: ${evs} vehicles\n`));
|
|
348
|
+
|
|
349
|
+
console.log(chalk.cyan.bold('β‘ Optimal Charging Stops:\n'));
|
|
350
|
+
stops.forEach((stop, idx) => {
|
|
351
|
+
console.log(chalk.white(` ${idx + 1}. ${stop.location}`));
|
|
352
|
+
console.log(chalk.gray(` Time: ${stop.time}`));
|
|
353
|
+
console.log(chalk.gray(` Price: $${stop.price.toFixed(4)}/kWh`));
|
|
354
|
+
console.log(chalk.green(` Savings: ${stop.savings}%\n`));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
console.log(chalk.cyan.bold('π° Cost Summary:\n'));
|
|
358
|
+
console.log(chalk.white(` Total Fleet Cost: $${totalCost.toFixed(2)}`));
|
|
359
|
+
console.log(chalk.green(` Average Savings: ${avgSavings.toFixed(1)}%`));
|
|
360
|
+
console.log(chalk.green(` COβ Saved: ${co2Saved} kg\n`));
|
|
361
|
+
|
|
362
|
+
// Prepare submission payload
|
|
363
|
+
const submissionPayload = {
|
|
364
|
+
type: 'fleet',
|
|
365
|
+
agent_name: agentName,
|
|
366
|
+
location: `${from} β ${to}`,
|
|
367
|
+
timestamp: Date.now(),
|
|
368
|
+
fleet_size: evs,
|
|
369
|
+
route: route.geometry, // GeoJSON LineString
|
|
370
|
+
stops: stops,
|
|
371
|
+
summary: {
|
|
372
|
+
total_distance_km: Math.round(route.distance),
|
|
373
|
+
total_cost_usd: parseFloat(totalCost.toFixed(2)),
|
|
374
|
+
savings_percent: Math.round(avgSavings),
|
|
375
|
+
co2_saved_kg: co2Saved,
|
|
376
|
+
duration_hours: Math.round(route.duration / 60)
|
|
377
|
+
},
|
|
378
|
+
simulation_basis: 'Task1 simulation + OSRM routing'
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Submit to oracle (blockchain)
|
|
382
|
+
const oracleSpinner = ora('Submitting to Solana blockchain...').start();
|
|
383
|
+
const oracleTx = await submitToOracle(walletAddress, submissionPayload);
|
|
384
|
+
oracleSpinner.succeed(chalk.green(`β Submitted to blockchain: ${oracleTx}`));
|
|
385
|
+
|
|
386
|
+
// Submit to dashboard API
|
|
387
|
+
try {
|
|
388
|
+
const dashboardSpinner = ora('Submitting to AgentOne dashboard...').start();
|
|
389
|
+
const apiUrl = process.env.DASHBOARD_API_URL || 'https://decharge-scout.vercel.app/api/agentone/fleet-submit';
|
|
390
|
+
|
|
391
|
+
const dashboardPayload = {
|
|
392
|
+
...submissionPayload,
|
|
393
|
+
wallet: walletAddress
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const response = await fetch(apiUrl, {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
headers: { 'Content-Type': 'application/json' },
|
|
399
|
+
body: JSON.stringify(dashboardPayload)
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (response.ok) {
|
|
403
|
+
dashboardSpinner.succeed(chalk.green('β Submitted to AgentOne dashboard'));
|
|
404
|
+
console.log(chalk.cyan('\nπΊοΈ Your fleet optimization has been added as a new layer on the AgentOne global map!'));
|
|
405
|
+
console.log(chalk.blue(' View at: https://decharge-scout.vercel.app/agentone\n'));
|
|
406
|
+
} else {
|
|
407
|
+
dashboardSpinner.warn(chalk.yellow(`β οΈ Dashboard API returned: ${response.status}`));
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.log(chalk.yellow(`β οΈ Dashboard submission failed: ${error.message}`));
|
|
411
|
+
console.log(chalk.gray(' (Blockchain submission was successful)\n'));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Award points
|
|
415
|
+
const basePoints = 5;
|
|
416
|
+
const bonusPoints = avgSavings > 25 ? 2 : 0;
|
|
417
|
+
const totalPoints = basePoints + bonusPoints;
|
|
418
|
+
|
|
419
|
+
awardPoints(walletAddress, totalPoints);
|
|
420
|
+
savePoints();
|
|
421
|
+
|
|
422
|
+
const currentPoints = getPoints(walletAddress);
|
|
423
|
+
console.log(chalk.magenta(`β Earned ${totalPoints} points! (${basePoints} base${bonusPoints > 0 ? ` + ${bonusPoints} bonus` : ''})`));
|
|
424
|
+
console.log(chalk.magenta(`β Total Points: ${currentPoints}\n`));
|
|
425
|
+
|
|
426
|
+
// Display ASCII map-like summary
|
|
427
|
+
console.log(chalk.cyan('ββββββββββββββββββββββββββββββββββββββββ'));
|
|
428
|
+
console.log(chalk.cyan.bold(' FLEET OPTIMIZATION MAP '));
|
|
429
|
+
console.log(chalk.cyan('ββββββββββββββββββββββββββββββββββββββββ'));
|
|
430
|
+
console.log(chalk.white(` ${from}`));
|
|
431
|
+
console.log(chalk.gray(' |'));
|
|
432
|
+
stops.forEach((stop, idx) => {
|
|
433
|
+
const marker = idx === stops.length - 1 ? 'βββ' : 'βββ';
|
|
434
|
+
console.log(chalk.green(` ${marker} β‘ Stop ${idx + 1}: ${stop.time} (${stop.savings}% savings)`));
|
|
435
|
+
if (idx < stops.length - 1) {
|
|
436
|
+
console.log(chalk.gray(' |'));
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
console.log(chalk.gray(' |'));
|
|
440
|
+
console.log(chalk.white(` ${to}`));
|
|
441
|
+
console.log(chalk.cyan('ββββββββββββββββββββββββββββββββββββββββ\n'));
|
|
442
|
+
|
|
443
|
+
console.log(chalk.green('β
Fleet optimization complete!\n'));
|
|
444
|
+
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.error(chalk.red(`\nβ Fleet optimization failed: ${error.message}\n`));
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
package/src/oracle.js
CHANGED
|
@@ -123,7 +123,26 @@ export async function submitToOracle(wallet, submissionData) {
|
|
|
123
123
|
* Anonymize submission data
|
|
124
124
|
*/
|
|
125
125
|
function anonymizeData(data) {
|
|
126
|
-
//
|
|
126
|
+
// Check if this is a fleet submission or regular submission
|
|
127
|
+
if (data.type === 'fleet') {
|
|
128
|
+
// Fleet submission - different structure
|
|
129
|
+
return {
|
|
130
|
+
agent_id: hashString(data.agent_name),
|
|
131
|
+
location_region: generalizeLocation(data.location),
|
|
132
|
+
timestamp: data.timestamp,
|
|
133
|
+
type: 'fleet',
|
|
134
|
+
fleet_size: data.fleet_size,
|
|
135
|
+
summary: {
|
|
136
|
+
...data.summary,
|
|
137
|
+
// Round values to reduce precision
|
|
138
|
+
total_cost_usd: parseFloat(data.summary.total_cost_usd.toFixed(2)),
|
|
139
|
+
savings_percent: Math.round(data.summary.savings_percent)
|
|
140
|
+
},
|
|
141
|
+
stops_count: data.stops?.length || 0
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Regular scout submission
|
|
127
146
|
const anonymized = {
|
|
128
147
|
agent_id: hashString(data.agent_name), // Hash the agent name
|
|
129
148
|
location_region: generalizeLocation(data.location), // Generalize location
|
package/src/weather-data.js
CHANGED
|
@@ -136,11 +136,39 @@ const KNOWN_CITIES = {
|
|
|
136
136
|
'dallas': { latitude: 32.776, longitude: -96.797, name: 'Dallas', country: 'United States', timezone: 'America/Chicago' },
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Get weather data for coordinates directly
|
|
141
|
+
* Used by fleet module for intermediate route points
|
|
142
|
+
*/
|
|
143
|
+
export async function getWeatherForCoordinates(latitude, longitude) {
|
|
144
|
+
// Get weather forecast directly without geocoding
|
|
145
|
+
const forecast = await fetchWeatherForecast(latitude, longitude, 'auto');
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
location: {
|
|
149
|
+
latitude,
|
|
150
|
+
longitude,
|
|
151
|
+
name: `${latitude.toFixed(2)},${longitude.toFixed(2)}`,
|
|
152
|
+
country: 'ROUTE',
|
|
153
|
+
timezone: 'auto'
|
|
154
|
+
},
|
|
155
|
+
forecast
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
139
159
|
/**
|
|
140
160
|
* Get weather data for a location
|
|
141
161
|
* Throws error if geocoding fails - caller should handle user interaction
|
|
142
162
|
*/
|
|
143
163
|
export async function getWeatherForLocation(location) {
|
|
164
|
+
// Check if location is already in coordinate format (lat,lon)
|
|
165
|
+
const coordMatch = location.match(/^(-?\d+\.?\d*),\s*(-?\d+\.?\d*)$/);
|
|
166
|
+
if (coordMatch) {
|
|
167
|
+
const lat = parseFloat(coordMatch[1]);
|
|
168
|
+
const lon = parseFloat(coordMatch[2]);
|
|
169
|
+
return getWeatherForCoordinates(lat, lon);
|
|
170
|
+
}
|
|
171
|
+
|
|
144
172
|
// Try geocoding - throw error if fails
|
|
145
173
|
const coords = await getCoordinatesFromLocation(location);
|
|
146
174
|
console.log(`π Location: ${coords.name}, ${coords.country} (${coords.latitude}, ${coords.longitude})`);
|