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_config | fn from_config(path: impl AsRef<Path>) -> Result<Self> | 从 TOML 加载 |
ApiServer::with_config | fn with_config(config: ApiConfig) -> Self | 编程式构造 |
ApiServer::listen | async fn listen(self, addr: &str) -> Result<()> | 启动 HTTP 服务 |
ApiClient::builder | fn builder() -> ApiClientBuilder | 构造客户端 |
ApiClient::list_networks | async fn list_networks(&self) -> Result<Vec<NetworkSummary>> | 列出网络 |
ApiClient::run_powerflow | async fn run_powerflow(&self, network_id: &str) -> Result<PowerFlowResultDto> | 触发潮流 |
ApiClient::create_agent | async fn create_agent(&self, req: CreateAgentRequest) -> Result<AgentDto> | 创建 Agent |
ApiClient::execute_command | async fn execute_command(&self, cmd: CommandRequest) -> Result<CommandResultDto> | 执行命令 |
ApiClient::query_timeseries | async 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/stream | SSE 实时推送 |
| GET | /ws | WebSocket 事件流 |
| POST | /graphql | GraphQL 查询 |
| GET | /openapi.json | OpenAPI 3.0 schema |
配置参数表
ApiConfig 字段
| 字段 | 类型 | 默认值 | 说明 |
|---|
bind | String | ”0.0.0.0:8080” | 监听地址 |
workers | usize | 4 | 工作线程数 |
enable_graphql | bool | true | 启用 GraphQL |
enable_websocket | bool | true | 启用 WebSocket |
enable_sse | bool | true | 启用 SSE |
cors_origins | Vec | [”*“] | CORS 允许来源 |
openapi_path | String | ”/openapi.json” | OpenAPI schema 路径 |
性能指标
测试环境:4 核 / 8GB / Ubuntu 22.04 / Rust 1.78 release build。
| 操作 | 延迟 | 吞吐 |
|---|
| 健康检查 | < 1ms | 5万 req/s |
| 列出网络 | < 5ms | 1万 req/s |
| 触发潮流(IEEE-14) | < 15ms | 100 req/s |
| 时序查询(1万点) | < 20ms | 5000 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.x | 0.47.x | 新增 SSE 与 GraphQL 端点 |
| 0.46.x | 0.46.x | WebSocket 事件流 |
| 0.45.x (LTS) | 0.45.x | LTS 版本,至 2028 年安全更新 |
相关文档