跳到主内容

eneros-api

Crate 索引

eneros-api

eneros-api 提供 EnerOS 的 CLI 工具 enerosctl 与四种 API 服务(REST / GraphQL / WebSocket / SSE),是用户与系统交互的主要入口。HTTP 服务基于 axum,CLI 基于 clap,两者共享同一套业务逻辑。所有写操作经 eneros-gateway 二次校验后落盘。

职责描述

eneros-api 承担以下核心职责:

  • REST API/api/v1/* 端点,覆盖网络管理、潮流计算、Agent 控制、时序查询、命令执行
  • GraphQL API/graphql 端点,支持声明式查询与字段裁剪,由 eneros-graphql crate 提供
  • WebSocket 推送:实时事件流,订阅拓扑变更、告警、Agent 状态等
  • SSE 推送:Server-Sent Events,浏览器友好的实时数据流
  • CLI 工具enerosctl 命令行工具,与 REST API 共享业务逻辑
  • OpenAPI 3.0:自动生成 OpenAPI schema,便于客户端 SDK 生成
  • 统一鉴权:Bearer Token + mTLS 双重鉴权
  • 限流:与 eneros-gateway 共享限流策略

关键类型与接口

ApiServer

use axum::Router;
use eneros_gateway::SafetyGateway;
use eneros_topology::NetworkGraph;

pub struct ApiServer {
    router: Router,
    gateway: SafetyGateway,
    config: ApiConfig,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ApiConfig {
    pub bind: String,
    pub workers: usize,
    pub enable_graphql: bool,
    pub enable_websocket: bool,
    pub enable_sse: bool,
    pub cors_origins: Vec<String>,
    pub openapi_path: String,
}

impl Default for ApiConfig {
    fn default() -> Self {
        Self {
            bind: "0.0.0.0:8080".into(),
            workers: 4,
            enable_graphql: true,
            enable_websocket: true,
            enable_sse: true,
            cors_origins: vec!["*".into()],
            openapi_path: "/openapi.json".into(),
        }
    }
}

impl ApiServer {
    pub fn from_config(path: impl AsRef<Path>) -> Result<Self> {
        let text = std::fs::read_to_string(path.as_ref())?;
        let config: ApiConfig = toml::from_str(&text)?;
        Ok(Self {
            router: Self::build_router(&config),
            gateway: SafetyGateway::from_config("gateway.toml")?,
            config,
        })
    }

    pub fn with_config(config: ApiConfig) -> Self {
        Self {
            router: Self::build_router(&config),
            gateway: SafetyGateway::default(),
            config,
        }
    }

    fn build_router(config: &ApiConfig) -> Router {
        let mut router = Router::new()
            .route("/api/v1/networks", get(list_networks).post(create_network))
            .route("/api/v1/networks/:id", get(get_network).delete(delete_network))
            .route("/api/v1/powerflow/:network_id", post(run_powerflow))
            .route("/api/v1/agents", get(list_agents).post(create_agent))
            .route("/api/v1/agents/:id", get(get_agent).delete(delete_agent))
            .route("/api/v1/commands", post(execute_command))
            .route("/api/v1/timeseries/query", post(query_timeseries))
            .route("/api/v1/alarms", get(list_alarms))
            .route("/api/v1/health", get(health_check));

        if config.enable_graphql {
            router = router.route("/graphql", post(graphql_handler).get(graphql_playground));
        }
        if config.enable_websocket {
            router = router.route("/ws", get(websocket_handler));
        }
        if config.enable_sse {
            router = router.route("/api/v1/stream", get(sse_handler));
        }
        router
    }

    pub async fn listen(self, addr: &str) -> Result<()> {
        let listener = tokio::net::TcpListener::bind(addr).await?;
        axum::serve(listener, self.router).await?;
        Ok(())
    }
}

ApiClient

pub struct ApiClient {
    endpoint: String,
    token: Option<String>,
    http: reqwest::Client,
}

pub struct ApiClientBuilder {
    endpoint: Option<String>,
    token: Option<String>,
    timeout: std::time::Duration,
}

impl ApiClientBuilder {
    pub fn new() -> Self {
        Self {
            endpoint: None,
            token: None,
            timeout: std::time::Duration::from_secs(30),
        }
    }

    pub fn endpoint(mut self, e: impl Into<String>) -> Self {
        self.endpoint = Some(e.into());
        self
    }

    pub fn token(mut self, t: impl Into<String>) -> Self {
        self.token = Some(t.into());
        self
    }

    pub fn timeout(mut self, t: std::time::Duration) -> Self {
        self.timeout = t;
        self
    }

    pub fn build(self) -> Result<ApiClient> {
        let endpoint = self.endpoint.ok_or_else(|| EnerOSError::Internal("endpoint required".into()))?;
        let mut http = reqwest::Client::builder().timeout(self.timeout);
        if let Some(token) = &self.token {
            http = http.default_headers(headers_with_bearer(token));
        }
        Ok(ApiClient {
            endpoint,
            token: self.token,
            http: http.build()?,
        })
    }
}

impl ApiClient {
    pub fn builder() -> ApiClientBuilder {
        ApiClientBuilder::new()
    }

    pub async fn list_networks(&self) -> Result<Vec<NetworkSummary>> {
        let url = format!("{}/api/v1/networks", self.endpoint);
        let resp = self.http.get(&url).send().await?.json().await?;
        Ok(resp)
    }

    pub async fn run_powerflow(&self, network_id: &str) -> Result<PowerFlowResultDto> {
        let url = format!("{}/api/v1/powerflow/{}", self.endpoint, network_id);
        let resp = self.http.post(&url).send().await?.json().await?;
        Ok(resp)
    }

    pub async fn create_agent(&self, req: CreateAgentRequest) -> Result<AgentDto> {
        let url = format!("{}/api/v1/agents", self.endpoint);
        let resp = self.http.post(&url).json(&req).send().await?.json().await?;
        Ok(resp)
    }

    pub async fn execute_command(&self, cmd: CommandRequest) -> Result<CommandResultDto> {
        let url = format!("{}/api/v1/commands", self.endpoint);
        let resp = self.http.post(&url).json(&cmd).send().await?.json().await?;
        Ok(resp)
    }

    pub async fn query_timeseries(&self, q: TimeSeriesQuery) -> Result<Vec<PointDto>> {
        let url = format!("{}/api/v1/timeseries/query", self.endpoint);
        let resp = self.http.post(&url).json(&q).send().await?.json().await?;
        Ok(resp)
    }
}

CLI

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "enerosctl", version = "0.47.0", about = "EnerOS CLI")]
pub struct Cli {
    #[command(subcommand)]
    pub command: CliCommand,

    #[arg(long, default_value = "http://localhost:8080")]
    pub endpoint: String,

    #[arg(long)]
    pub token: Option<String>,
}

#[derive(Subcommand)]
pub enum CliCommand {
    /// 网络管理
    Network {
        #[command(subcommand)]
        action: NetworkAction,
    },
    /// Agent 管理
    Agent {
        #[command(subcommand)]
        action: AgentAction,
    },
    /// 潮流计算
    Powerflow {
        network_id: String,
        #[arg(long, default_value = "newton_raphson")]
        method: String,
    },
    /// 时序查询
    Query {
        #[arg(long)]
        metric: String,
        #[arg(long)]
        from: String,
        #[arg(long)]
        to: String,
    },
    /// 实时事件流(WebSocket)
    Stream {
        #[arg(long, default_value = "alarms")]
        topic: String,
    },
}

#[derive(Subcommand)]
pub enum NetworkAction {
    List,
    Create { name: String },
    Delete { id: String },
    Show { id: String },
}

#[derive(Subcommand)]
pub enum AgentAction {
    List,
    Create {
        #[arg(long)]
        r#type: String,
        #[arg(long)]
        name: String,
    },
    Delete { id: String },
    Start { id: String },
    Stop { id: String },
}

DTO 类型

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct NetworkSummary {
    pub id: String,
    pub name: String,
    pub bus_count: usize,
    pub branch_count: usize,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PowerFlowResultDto {
    pub network_id: String,
    pub converged: bool,
    pub iterations: usize,
    pub duration_ms: f64,
    pub buses: Vec<BusSolutionDto>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BusSolutionDto {
    pub id: String,
    pub voltage_magnitude: f64,
    pub voltage_angle_rad: f64,
    pub p_pu: f64,
    pub q_pu: f64,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CommandRequest {
    pub agent_id: String,
    pub action: String,
    pub params: serde_json::Value,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CommandResultDto {
    pub command_id: String,
    pub status: String,
    pub duration_ms: f64,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CreateAgentRequest {
    pub r#type: String,
    pub name: String,
    pub jurisdiction: Option<serde_json::Value>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AgentDto {
    pub id: String,
    pub r#type: String,
    pub name: String,
    pub status: String,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TimeSeriesQuery {
    pub metric: String,
    pub from: chrono::DateTime<chrono::Utc>,
    pub to: chrono::DateTime<chrono::Utc>,
    pub aggregation: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PointDto {
    pub timestamp: chrono::DateTime<chrono::Utc>,
    pub value: f64,
}

核心 API

方法签名说明
ApiServer::from_configfn from_config(path: impl AsRef<Path>) -> Result<Self>从 TOML 加载
ApiServer::with_configfn with_config(config: ApiConfig) -> Self编程式构造
ApiServer::listenasync fn listen(self, addr: &str) -> Result<()>启动 HTTP 服务
ApiClient::builderfn builder() -> ApiClientBuilder构造客户端
ApiClient::list_networksasync fn list_networks(&self) -> Result<Vec<NetworkSummary>>列出网络
ApiClient::run_powerflowasync fn run_powerflow(&self, network_id: &str) -> Result<PowerFlowResultDto>触发潮流
ApiClient::create_agentasync fn create_agent(&self, req: CreateAgentRequest) -> Result<AgentDto>创建 Agent
ApiClient::execute_commandasync fn execute_command(&self, cmd: CommandRequest) -> Result<CommandResultDto>执行命令
ApiClient::query_timeseriesasync fn query_timeseries(&self, q: TimeSeriesQuery) -> Result<Vec<PointDto>>时序查询

使用示例

CLI 命令

# 列出所有网络
enerosctl network list

# 创建网络
enerosctl network create --name "ieee14"

# 查看网络详情
enerosctl network show net_8f3a2b

# 创建 Agent
enerosctl agent create --type dispatch --name "dispatch_01"

# 启动 Agent
enerosctl agent start dispatch_01

# 触发潮流计算
enerosctl powerflow net_8f3a2b --method newton_raphson

# 时序查询
enerosctl query \
  --metric branch_loading \
  --from 2026-07-06T00:00:00Z \
  --to 2026-07-06T08:00:00Z

# 订阅实时告警流
enerosctl stream --topic alarms

Rust SDK 调用

use eneros_api::ApiClient;

let client = ApiClient::builder()
    .endpoint("http://localhost:8080")
    .token("<bearer-token>")
    .build()?;

let networks = client.list_networks().await?;
for n in &networks {
    println!("网络: {} ({} 母线)", n.name, n.bus_count);
}

let result = client.run_powerflow("net_8f3a2b").await?;
println!(
    "潮流结果: 收敛={} 迭代={} 耗时={:.2}ms",
    result.converged, result.iterations, result.duration_ms
);

for bus in &result.buses {
    println!(
        "母线 {}: V={:.4} θ={:.4}rad",
        bus.id, bus.voltage_magnitude, bus.voltage_angle_rad
    );
}

创建 Agent 并执行命令

use eneros_api::{ApiClient, CreateAgentRequest, CommandRequest};

let client = ApiClient::builder()
    .endpoint("http://localhost:8080")
    .token("<bearer-token>")
    .build()?;

let agent = client.create_agent(CreateAgentRequest {
    r#type: "dispatch".into(),
    name: "dispatch_01".into(),
    jurisdiction: None,
}).await?;

println!("Agent 创建: id={}", agent.id);

let result = client.execute_command(CommandRequest {
    agent_id: agent.id.clone(),
    action: "SetGeneration".into(),
    params: serde_json::json!({"bus": "bus_2", "mw": 50.0}),
}).await?;

println!("命令结果: status={} duration={:.2}ms", result.status, result.duration_ms);

时序查询

use eneros_api::{ApiClient, TimeSeriesQuery};
use chrono::{Utc, Duration};

let client = ApiClient::builder()
    .endpoint("http://localhost:8080")
    .build()?;

let now = Utc::now();
let points = client.query_timeseries(TimeSeriesQuery {
    metric: "branch_loading".into(),
    from: now - Duration::hours(8),
    to: now,
    aggregation: Some("avg_5min".into()),
}).await?;

for p in &points {
    println!("{}: {:.2}%", p.timestamp, p.value);
}

启动 API 服务

use eneros_api::ApiServer;

let server = ApiServer::from_config("api.toml")?;
server.listen("0.0.0.0:8080").await?;

REST API 示例

# 列出网络
curl http://localhost:8080/api/v1/networks

# 触发潮流
curl -X POST http://localhost:8080/api/v1/powerflow/net_8f3a2b

# 创建 Agent
curl -X POST http://localhost:8080/api/v1/agents \
  -H "Content-Type: application/json" \
  -d '{"type":"dispatch","name":"dispatch_01"}'

# 时序查询
curl -X POST http://localhost:8080/api/v1/timeseries/query \
  -H "Content-Type: application/json" \
  -d '{"metric":"branch_loading","from":"2026-07-06T00:00:00Z","to":"2026-07-06T08:00:00Z"}'

# SSE 实时推送
curl -N http://localhost:8080/api/v1/stream?topic=alarms

# GraphQL 查询
curl -X POST http://localhost:8080/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ networks { id name busCount } }"}'

REST API 端点表

方法路径说明
GET/api/v1/networks列出所有网络
POST/api/v1/networks创建网络
GET/api/v1/networks/:id获取网络详情
DELETE/api/v1/networks/:id删除网络
POST/api/v1/powerflow/:network_id触发潮流计算
GET/api/v1/agents列出 Agent
POST/api/v1/agents创建 Agent
GET/api/v1/agents/:id获取 Agent 详情
DELETE/api/v1/agents/:id删除 Agent
POST/api/v1/commands执行命令
POST/api/v1/timeseries/query时序查询
GET/api/v1/alarms列出告警
GET/api/v1/health健康检查
GET/api/v1/streamSSE 实时推送
GET/wsWebSocket 事件流
POST/graphqlGraphQL 查询
GET/openapi.jsonOpenAPI 3.0 schema

配置参数表

ApiConfig 字段

字段类型默认值说明
bindString”0.0.0.0:8080”监听地址
workersusize4工作线程数
enable_graphqlbooltrue启用 GraphQL
enable_websocketbooltrue启用 WebSocket
enable_ssebooltrue启用 SSE
cors_originsVec[”*“]CORS 允许来源
openapi_pathString”/openapi.json”OpenAPI schema 路径

性能指标

测试环境:4 核 / 8GB / Ubuntu 22.04 / Rust 1.78 release build。

操作延迟吞吐
健康检查< 1ms5万 req/s
列出网络< 5ms1万 req/s
触发潮流(IEEE-14)< 15ms100 req/s
时序查询(1万点)< 20ms5000 req/s
SSE 推送(1000 订阅者)< 1ms 广播延迟-
WebSocket 推送< 500μs-

依赖关系

自身依赖

[dependencies]
eneros-core = { path = "../core", version = "0.47.0" }
eneros-gateway = { path = "../gateway", version = "0.47.0" }
eneros-topology = { path = "../topology", version = "0.47.0" }
eneros-powerflow = { path = "../powerflow", version = "0.47.0" }
eneros-agent = { path = "../agent", version = "0.47.0" }
eneros-timeseries = { path = "../timeseries", version = "0.47.0" }
eneros-graphql = { path = "../graphql", version = "0.47.0", optional = true }
axum = { version = "0.7", features = ["ws"] }
clap = { version = "4", features = ["derive"] }
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
tracing = "0.1"
chrono = { version = "0.4", features = ["serde"] }

[features]
default = ["graphql"]
graphql = ["dep:eneros-graphql"]

被依赖

  • eneros-dashboard — 仪表盘内嵌 API
  • eneros-edge — 边缘节点运行 API 实例

版本与兼容性

eneros-api 版本EnerOS 版本关键变更
0.47.x0.47.x新增 SSE 与 GraphQL 端点
0.46.x0.46.xWebSocket 事件流
0.45.x (LTS)0.45.xLTS 版本,至 2028 年安全更新

相关文档