eneros-api
eneros-api provides EnerOS’s CLI tool enerosctl and four API services (REST / GraphQL / WebSocket / SSE), serving as the primary entry point for user-system interaction. The HTTP service is based on axum, the CLI on clap, and both share the same business logic. All write operations go through secondary validation by eneros-gateway before being persisted.
Responsibilities
eneros-api handles the following core responsibilities:
- REST API:
/api/v1/* endpoints covering network management, Load Flow calculation, Agent control, time series query, and command execution
- GraphQL API:
/graphql endpoint supporting declarative queries and field pruning, provided by the eneros-graphql crate
- WebSocket Push: Real-time event streams, subscribing to topology changes, alarms, Agent status, etc.
- SSE Push: Server-Sent Events, browser-friendly real-time data streams
- CLI Tool:
enerosctl command-line tool, sharing business logic with the REST API
- OpenAPI 3.0: Auto-generated OpenAPI schema for easy client SDK generation
- Unified Authentication: Dual authentication with Bearer Token + mTLS
- Rate Limiting: Shares rate limiting policies with
eneros-gateway
Key Types and Interfaces
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 management
Network {
#[command(subcommand)]
action: NetworkAction,
},
/// Agent management
Agent {
#[command(subcommand)]
action: AgentAction,
},
/// Load Flow calculation
Powerflow {
network_id: String,
#[arg(long, default_value = "newton_raphson")]
method: String,
},
/// Time series query
Query {
#[arg(long)]
metric: String,
#[arg(long)]
from: String,
#[arg(long)]
to: String,
},
/// Real-time event stream (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 Types
#[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,
}
Core API
| Method | Signature | Description |
|---|
ApiServer::from_config | fn from_config(path: impl AsRef<Path>) -> Result<Self> | Load from TOML |
ApiServer::with_config | fn with_config(config: ApiConfig) -> Self | Programmatic construction |
ApiServer::listen | async fn listen(self, addr: &str) -> Result<()> | Start HTTP service |
ApiClient::builder | fn builder() -> ApiClientBuilder | Construct client |
ApiClient::list_networks | async fn list_networks(&self) -> Result<Vec<NetworkSummary>> | List networks |
ApiClient::run_powerflow | async fn run_powerflow(&self, network_id: &str) -> Result<PowerFlowResultDto> | Trigger Load Flow |
ApiClient::create_agent | async fn create_agent(&self, req: CreateAgentRequest) -> Result<AgentDto> | Create Agent |
ApiClient::execute_command | async fn execute_command(&self, cmd: CommandRequest) -> Result<CommandResultDto> | Execute command |
ApiClient::query_timeseries | async fn query_timeseries(&self, q: TimeSeriesQuery) -> Result<Vec<PointDto>> | Time series query |
Usage Examples
CLI Commands
# List all networks
enerosctl network list
# Create network
enerosctl network create --name "ieee14"
# View network details
enerosctl network show net_8f3a2b
# Create Agent
enerosctl agent create --type dispatch --name "dispatch_01"
# Start Agent
enerosctl agent start dispatch_01
# Trigger Load Flow calculation
enerosctl powerflow net_8f3a2b --method newton_raphson
# Time series query
enerosctl query \
--metric branch_loading \
--from 2026-07-06T00:00:00Z \
--to 2026-07-06T08:00:00Z
# Subscribe to real-time alarm stream
enerosctl stream --topic alarms
Rust SDK Calls
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!("Network: {} ({} buses)", n.name, n.bus_count);
}
let result = client.run_powerflow("net_8f3a2b").await?;
println!(
"Load Flow result: converged={} iterations={} duration={:.2}ms",
result.converged, result.iterations, result.duration_ms
);
for bus in &result.buses {
println!(
"Bus {}: V={:.4} θ={:.4}rad",
bus.id, bus.voltage_magnitude, bus.voltage_angle_rad
);
}
Create Agent and Execute Command
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 created: 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!("Command result: status={} duration={:.2}ms", result.status, result.duration_ms);
Time Series Query
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);
}
Start API Service
use eneros_api::ApiServer;
let server = ApiServer::from_config("api.toml")?;
server.listen("0.0.0.0:8080").await?;
REST API Examples
# List networks
curl http://localhost:8080/api/v1/networks
# Trigger Load Flow
curl -X POST http://localhost:8080/api/v1/powerflow/net_8f3a2b
# Create Agent
curl -X POST http://localhost:8080/api/v1/agents \
-H "Content-Type: application/json" \
-d '{"type":"dispatch","name":"dispatch_01"}'
# Time series query
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 real-time push
curl -N http://localhost:8080/api/v1/stream?topic=alarms
# GraphQL query
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ networks { id name busCount } }"}'
REST API Endpoint Table
| Method | Path | Description |
|---|
| GET | /api/v1/networks | List all networks |
| POST | /api/v1/networks | Create network |
| GET | /api/v1/networks/:id | Get network details |
| DELETE | /api/v1/networks/:id | Delete network |
| POST | /api/v1/powerflow/:network_id | Trigger Load Flow calculation |
| GET | /api/v1/agents | List Agents |
| POST | /api/v1/agents | Create Agent |
| GET | /api/v1/agents/:id | Get Agent details |
| DELETE | /api/v1/agents/:id | Delete Agent |
| POST | /api/v1/commands | Execute command |
| POST | /api/v1/timeseries/query | Time series query |
| GET | /api/v1/alarms | List alarms |
| GET | /api/v1/health | Health check |
| GET | /api/v1/stream | SSE real-time push |
| GET | /ws | WebSocket event stream |
| POST | /graphql | GraphQL query |
| GET | /openapi.json | OpenAPI 3.0 schema |
Configuration Parameter Table
ApiConfig Fields
| Field | Type | Default | Description |
|---|
bind | String | ”0.0.0.0:8080” | Listen address |
workers | usize | 4 | Number of worker threads |
enable_graphql | bool | true | Enable GraphQL |
enable_websocket | bool | true | Enable WebSocket |
enable_sse | bool | true | Enable SSE |
cors_origins | Vec | [”*“] | CORS allowed origins |
openapi_path | String | ”/openapi.json” | OpenAPI schema path |
Test environment: 4 cores / 8GB / Ubuntu 22.04 / Rust 1.78 release build.
| Operation | Latency | Throughput |
|---|
| Health check | < 1ms | 50k req/s |
| List networks | < 5ms | 10k req/s |
| Trigger Load Flow (IEEE-14) | < 15ms | 100 req/s |
| Time series query (10k points) | < 20ms | 5000 req/s |
| SSE push (1000 subscribers) | < 1ms broadcast latency | - |
| WebSocket push | < 500μs | - |
Dependencies
Own Dependencies
[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"]
Depended On By
eneros-dashboard — Dashboard embeds API
eneros-edge — Edge nodes run API instances
Version and Compatibility
| eneros-api Version | EnerOS Version | Key Changes |
|---|
| 0.47.x | 0.47.x | Added SSE and GraphQL endpoints |
| 0.46.x | 0.46.x | WebSocket event stream |
| 0.45.x (LTS) | 0.45.x | LTS version, security updates until 2028 |