vendor: update cargo-cxai-energy-0.1.0
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "d878deb8441897ecdd416011b49d2d2f6112e867"
|
||||
},
|
||||
"path_in_vcs": "crates/cxai-energy"
|
||||
}
|
||||
Generated
+1942
File diff suppressed because it is too large
Load Diff
+74
@@ -0,0 +1,74 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "cxai-energy"
|
||||
version = "0.1.0"
|
||||
authors = ["CxAI-LLM <agent@cxai-studio.com>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "ERCOT energy market client — DAM/RT prices, demand, wind/solar, SCADA, forecasting"
|
||||
homepage = "https://cxllm.io"
|
||||
readme = false
|
||||
license = "MIT"
|
||||
repository = "https://git.cxllm-studio.com/CxAI-LLM/CxAI.Rust"
|
||||
resolver = "2"
|
||||
|
||||
[lib]
|
||||
name = "cxai_energy"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.cxai-sdk]
|
||||
version = "0.1.0"
|
||||
registry-index = "sparse+https://git.cxllm-studio.com/api/packages/CxAI-LLM/cargo/"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12"
|
||||
features = [
|
||||
"json",
|
||||
"rustls-tls",
|
||||
"stream",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1"
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1"
|
||||
features = ["full"]
|
||||
|
||||
[dependencies.tracing]
|
||||
version = "0.1"
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
version = "1"
|
||||
features = [
|
||||
"full",
|
||||
"test-util",
|
||||
"macros",
|
||||
]
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "cxai-energy"
|
||||
description = "ERCOT energy market client — DAM/RT prices, demand, wind/solar, SCADA, forecasting"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cxai-sdk = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||
@@ -0,0 +1,63 @@
|
||||
use cxai_sdk::models::energy::*;
|
||||
use tracing::debug;
|
||||
|
||||
/// High-level ERCOT energy market client.
|
||||
pub struct ErcotClient {
|
||||
platform: cxai_sdk::CxPlatform,
|
||||
}
|
||||
|
||||
impl ErcotClient {
|
||||
pub fn new(platform: cxai_sdk::CxPlatform) -> Self {
|
||||
Self { platform }
|
||||
}
|
||||
|
||||
/// Get day-ahead market prices for a settlement point.
|
||||
pub async fn dam_prices(&self, settlement_point: &str) -> cxai_sdk::CxResult<Vec<DamPrice>> {
|
||||
debug!(settlement_point, "Fetching DAM prices");
|
||||
self.platform.energy().dam_prices(settlement_point).await
|
||||
}
|
||||
|
||||
/// Get real-time prices for a settlement point.
|
||||
pub async fn rt_prices(&self, settlement_point: &str) -> cxai_sdk::CxResult<Vec<RtPrice>> {
|
||||
debug!(settlement_point, "Fetching RT prices");
|
||||
self.platform.energy().rt_prices(settlement_point).await
|
||||
}
|
||||
|
||||
/// Get current system demand.
|
||||
pub async fn system_demand(&self) -> cxai_sdk::CxResult<SystemDemand> {
|
||||
self.platform.energy().system_demand().await
|
||||
}
|
||||
|
||||
/// Get current wind generation.
|
||||
pub async fn wind(&self) -> cxai_sdk::CxResult<WindGeneration> {
|
||||
self.platform.energy().wind_generation().await
|
||||
}
|
||||
|
||||
/// Get current solar generation.
|
||||
pub async fn solar(&self) -> cxai_sdk::CxResult<SolarGeneration> {
|
||||
self.platform.energy().solar_generation().await
|
||||
}
|
||||
|
||||
/// Get full market summary.
|
||||
pub async fn market_summary(&self) -> cxai_sdk::CxResult<MarketSummary> {
|
||||
self.platform.energy().market_summary().await
|
||||
}
|
||||
|
||||
/// Get ML prediction for energy data.
|
||||
pub async fn predict(
|
||||
&self,
|
||||
prediction_type: &str,
|
||||
model: &str,
|
||||
target_date: &str,
|
||||
target_hour: u8,
|
||||
) -> cxai_sdk::CxResult<ErcotMlPrediction> {
|
||||
debug!(
|
||||
prediction_type,
|
||||
model, target_date, target_hour, "ERCOT predict"
|
||||
);
|
||||
self.platform
|
||||
.energy()
|
||||
.predict(prediction_type, model, target_date, target_hour)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod ercot;
|
||||
pub mod market;
|
||||
pub mod scada;
|
||||
|
||||
pub use ercot::ErcotClient;
|
||||
pub use market::MarketAnalyzer;
|
||||
pub use scada::ScadaClient;
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
use cxai_sdk::models::energy::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Market analysis utilities for ERCOT data.
|
||||
pub struct MarketAnalyzer;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpreadAnalysis {
|
||||
pub avg_dam: f64,
|
||||
pub avg_rt: f64,
|
||||
pub spread: f64,
|
||||
pub max_spread: f64,
|
||||
pub arbitrage_opportunities: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RenewableMix {
|
||||
pub wind_mw: f64,
|
||||
pub solar_mw: f64,
|
||||
pub total_renewable_mw: f64,
|
||||
pub demand_mw: f64,
|
||||
pub renewable_percentage: f64,
|
||||
}
|
||||
|
||||
impl MarketAnalyzer {
|
||||
/// Calculate DAM/RT price spread.
|
||||
pub fn spread(dam_prices: &[DamPrice], rt_prices: &[RtPrice]) -> SpreadAnalysis {
|
||||
let avg_dam = if dam_prices.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
dam_prices.iter().map(|p| p.price).sum::<f64>() / dam_prices.len() as f64
|
||||
};
|
||||
|
||||
let avg_rt = if rt_prices.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
rt_prices.iter().map(|p| p.price).sum::<f64>() / rt_prices.len() as f64
|
||||
};
|
||||
|
||||
let spreads: Vec<f64> = dam_prices
|
||||
.iter()
|
||||
.zip(rt_prices.iter())
|
||||
.map(|(d, r)| (d.price - r.price).abs())
|
||||
.collect();
|
||||
|
||||
let max_spread = spreads.iter().cloned().fold(0.0_f64, f64::max);
|
||||
let arbitrage_opportunities = spreads.iter().filter(|&&s| s > 10.0).count();
|
||||
|
||||
SpreadAnalysis {
|
||||
avg_dam,
|
||||
avg_rt,
|
||||
spread: avg_dam - avg_rt,
|
||||
max_spread,
|
||||
arbitrage_opportunities,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate renewable energy mix.
|
||||
pub fn renewable_mix(
|
||||
wind: &WindGeneration,
|
||||
solar: &SolarGeneration,
|
||||
demand: &SystemDemand,
|
||||
) -> RenewableMix {
|
||||
let total = wind.generation_mw + solar.generation_mw;
|
||||
let pct = if demand.demand_mw > 0.0 {
|
||||
(total / demand.demand_mw) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
RenewableMix {
|
||||
wind_mw: wind.generation_mw,
|
||||
solar_mw: solar.generation_mw,
|
||||
total_renewable_mw: total,
|
||||
demand_mw: demand.demand_mw,
|
||||
renewable_percentage: pct,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::{NaiveDate, Utc};
|
||||
|
||||
#[test]
|
||||
fn spread_calculation() {
|
||||
let dam = vec![DamPrice {
|
||||
settlement_point: "HB_HOUSTON".into(),
|
||||
delivery_date: NaiveDate::from_ymd_opt(2026, 4, 20).unwrap(),
|
||||
hour_ending: 14,
|
||||
price: 45.0,
|
||||
timestamp: None,
|
||||
}];
|
||||
let rt = vec![RtPrice {
|
||||
settlement_point: "HB_HOUSTON".into(),
|
||||
price: 52.0,
|
||||
interval: "15min".into(),
|
||||
timestamp: Utc::now(),
|
||||
}];
|
||||
|
||||
let analysis = MarketAnalyzer::spread(&dam, &rt);
|
||||
assert_eq!(analysis.avg_dam, 45.0);
|
||||
assert_eq!(analysis.avg_rt, 52.0);
|
||||
assert!((analysis.spread - (-7.0)).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renewable_mix_calculation() {
|
||||
let wind = WindGeneration {
|
||||
generation_mw: 15000.0,
|
||||
capacity_mw: 40000.0,
|
||||
timestamp: Utc::now(),
|
||||
};
|
||||
let solar = SolarGeneration {
|
||||
generation_mw: 8000.0,
|
||||
capacity_mw: 20000.0,
|
||||
timestamp: Utc::now(),
|
||||
};
|
||||
let demand = SystemDemand {
|
||||
demand_mw: 60000.0,
|
||||
timestamp: Utc::now(),
|
||||
forecast_mw: None,
|
||||
};
|
||||
|
||||
let mix = MarketAnalyzer::renewable_mix(&wind, &solar, &demand);
|
||||
assert_eq!(mix.total_renewable_mw, 23000.0);
|
||||
assert!((mix.renewable_percentage - 38.333333333333336).abs() < 0.001);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use cxai_sdk::models::energy::ScadaSnapshot;
|
||||
use tracing::debug;
|
||||
|
||||
/// SCADA telemetry client for battery storage systems.
|
||||
pub struct ScadaClient {
|
||||
platform: cxai_sdk::CxPlatform,
|
||||
}
|
||||
|
||||
impl ScadaClient {
|
||||
pub fn new(platform: cxai_sdk::CxPlatform) -> Self {
|
||||
Self { platform }
|
||||
}
|
||||
|
||||
/// Get the latest SCADA snapshot for a device.
|
||||
pub async fn snapshot(&self, device_id: &str) -> cxai_sdk::CxResult<ScadaSnapshot> {
|
||||
debug!(device_id, "Fetching SCADA snapshot");
|
||||
self.platform.energy().scada_snapshot(device_id).await
|
||||
}
|
||||
|
||||
/// Check if a battery is within safe operating parameters.
|
||||
pub fn is_safe(snapshot: &ScadaSnapshot) -> bool {
|
||||
snapshot.soc_percent >= 5.0
|
||||
&& snapshot.soc_percent <= 95.0
|
||||
&& snapshot.temperature_c < 55.0
|
||||
&& snapshot.soh_percent > 70.0
|
||||
}
|
||||
|
||||
/// Calculate charge/discharge efficiency from a snapshot.
|
||||
pub fn efficiency(snapshot: &ScadaSnapshot) -> f64 {
|
||||
if snapshot.voltage_v == 0.0 || snapshot.current_a == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let power_calc = snapshot.voltage_v * snapshot.current_a / 1000.0;
|
||||
if power_calc == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
(snapshot.power_kw / power_calc).abs().min(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
|
||||
fn test_snapshot() -> ScadaSnapshot {
|
||||
ScadaSnapshot {
|
||||
device_id: "BESS-001".into(),
|
||||
soc_percent: 65.0,
|
||||
soh_percent: 92.0,
|
||||
power_kw: 450.0,
|
||||
voltage_v: 800.0,
|
||||
current_a: 580.0,
|
||||
temperature_c: 32.0,
|
||||
timestamp: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_battery() {
|
||||
assert!(ScadaClient::is_safe(&test_snapshot()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsafe_temperature() {
|
||||
let mut s = test_snapshot();
|
||||
s.temperature_c = 60.0;
|
||||
assert!(!ScadaClient::is_safe(&s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn efficiency_calculation() {
|
||||
let s = test_snapshot();
|
||||
let eff = ScadaClient::efficiency(&s);
|
||||
assert!(eff > 0.0 && eff <= 1.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user