Open & Interoperable
Open & Interoperable is the ecosystem philosophy of EnerOS: based on international power industry standard protocols, it provides multiple API types and data exchange formats to ensure seamless interoperability with existing SCADA, EMS, DMS, and distribution automation systems. EnerOS does not lock in users; all data can be exported to standard formats, and all interfaces follow open specifications.
Design Motivation
Protocol Fragmentation in Power Systems
Power systems have evolved over decades and contain numerous heterogeneous protocols and systems:
| System | Main Protocols | Data Format | Communication Mode |
|---|---|---|---|
| Dispatch automation (EMS) | IEC 60870-5-104 | Proprietary | Master-slave |
| Substation automation | IEC 61850 | MMS/GOOSE | Publish-subscribe |
| Distribution automation (DMS) | IEC 60870-5-104 / DNP3 | Proprietary | Master-slave |
| Telecontrol RTU | IEC 60870-5-101/104 | Proprietary | Master-slave |
| Smart meters | DL/T 698.45 / Modbus | Proprietary | Master-slave |
| Grid model | IEC 61970 (CIM) | XML/RDF | File |
| Distribution model | IEC 61968 (CIM) | XML/JSON | File |
| Wind/Solar | IEC 61400-25 | MMS | Publish-subscribe |
| IoT devices | MQTT / CoAP | JSON | Publish-subscribe |
Traditional solutions require dedicated adapters for each protocol, resulting in high integration costs and difficult maintenance.
Open & Interoperable Solution
EnerOS has built-in mainstream power protocol adapters with a unified external interface:
┌─────────────────────────────────────────────────────────┐
│ External Systems │
│ SCADA │ EMS │ DMS │ IoT │ Meters │ Wind Farm │
└────┬───────┬───────┬───────┬───────┬───────┬────────────┘
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────┐
│ EnerOS Protocol Adapter Layer │
│ IEC104 │ IEC61850 │ DNP3 │ Modbus │ MQTT │ DL/T698 │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Unified Data Model │
│ Standardized based on IEC 61970/61968 CIM │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ EnerOS Power-Native Kernel │
└─────────────────────────────────────────────────────────┘
Supported Protocols
Protocol Overview
| Protocol | Standard Number | Applicable Scenario | Communication Mode | Crate |
|---|---|---|---|---|
| IEC 61850 | Substation automation | Substation devices | MMS/GOOSE/SV | eneros-protocol-iec61850 |
| IEC 60870-5-104 | Telecontrol protocol | Dispatch master-substation | Master-slave | eneros-protocol-iec104 |
| IEC 60870-5-101 | Telecontrol protocol (serial) | Legacy RTU | Master-slave | eneros-protocol-iec101 |
| IEC 61970 (CIM) | Grid model | EMS model exchange | File | eneros-protocol-cim |
| IEC 61968 (CIM) | Distribution model | DMS model exchange | File | eneros-protocol-cim |
| DNP3 | Distribution automation | North American distribution | Master-slave | eneros-protocol-dnp3 |
| Modbus | Industrial devices | General industrial devices | Master-slave | eneros-protocol-modbus |
| IEC 61400-25 | Wind farms | Wind turbine monitoring | MMS | eneros-protocol-iec61400 |
| DL/T 698.45 | Smart meters | Chinese meters | Master-slave | eneros-protocol-dlt698 |
| MQTT 5.0 | IoT | IoT devices | Publish-subscribe | eneros-protocol-mqtt |
| OPC UA | Industrial unified architecture | General industrial | Client-server | eneros-protocol-opcua |
IEC 61850
International standard for power substation automation, supporting MMS, GOOSE, and SV communication modes:
use eneros_protocol_iec61850::{IedClient, DataObject, Quality};
// Connect to IED (Intelligent Electronic Device)
let mut client = IedClient::connect("192.168.1.100:102")?
.authenticate("admin", "password")?
.timeout(std::time::Duration::from_secs(5));
// Read measurement value
let voltage = client.read("LD0/LLN0.Vol.sv").await?;
println!("Voltage: {} kV, Quality: {:?}", voltage.value, voltage.quality);
// Read double position signal (breaker status)
let breaker_pos = client.read("LD0/XCBR1.Pos.stVal").await?;
println!("Breaker status: {:?}", breaker_pos.value);
// Control breaker
client.control("LD0/XCBR1.Pos.Oper", ControlAction::Close).await?;
// Subscribe to GOOSE events
client.subscribe_goose("LD0/LLN0.GOCB1", |event| {
println!("GOOSE event: {:?}", event);
}).await?;
IEC 60870-5-104
Telecontrol protocol between dispatch control center and substations:
use eneros_protocol_iec104::{Iec104Master, AsduType, CauseOfTransmission};
// Create master station
let mut master = Iec104Master::connect("10.0.0.1:2404")?
.common_address_size(2)
.io_address_size(3)
.timeout(t1: 15s, t2: 10s, t3: 20s);
// General interrogation (read all data)
master.interrogation(AsduType::C_IC_NA_1, 1, CauseOfTransmission::Activation).await?;
// Single point remote control
master.send_command(AsduType::C_SC_NA_1, 1, true).await?;
// Double point remote control (close/open)
master.send_command(AsduType::C_DC_NA_1, 1, DoubleCommand::Close).await?;
// Set point command (set value)
master.set_point(AsduType::C_SE_NA_1, 1, 80.0).await?;
// Listen to telemetry changes
master.on_measurement(|asdu| {
println!("Type: {:?}, Address: {}, Value: {:?}",
asdu.type_id, asdu.ioa, asdu.values);
}).await?;
DNP3
North American distribution automation standard protocol:
use eneros_protocol_dnp3::{Dnp3Master, Command, PointType};
let mut master = Dnp3Master::connect_tcp("10.0.0.2:20000")?
.master_address(1)
.timeout(std::time::Duration::from_secs(5));
// Read analog values
let analog = master.read_analog_input(1, 10).await?;
let voltage = analog[0].value;
// Read digital values
let binary = master.read_binary_input(1, 5).await?;
// Control
master.control(Command::Operate, PointType::BinaryOutput, 1, true).await?;
Modbus
General protocol for industrial devices:
use eneros_protocol_modbus::{ModbusClient, Register};
// TCP connection
let mut client = ModbusClient::connect_tcp("192.168.1.50:502")?;
// RTU serial connection
let mut rtu_client = ModbusClient::connect_rtu("/dev/ttyUSB0", 9600)?;
// Read holding registers
let registers = client.read_holding_registers(0, 10).await?;
let voltage = f32::from_bits(registers[0] as u32 | (registers[1] as u32) << 16);
// Write multiple registers
client.write_multiple_registers(10, &[0x1234, 0x5678]).await?;
// Read coil status
let coils = client.read_coils(0, 16).await?;
DL/T 698.45
Chinese smart meter communication protocol:
use eneros_protocol_dlt698::Dlt698Client;
let mut client = Dlt698Client::connect("/dev/ttyUSB0")?
.baud_rate(2400)
.address("00-00-00-00");
// Read meter readings
let reading = client.read_meter("00-00-00-00").await?;
println!("Current active energy: {} kWh", reading.active_energy);
println!("Current reactive energy: {} kvarh", reading.reactive_energy);
println!("Voltage: {} V", reading.voltage);
println!("Current: {} A", reading.current);
// Read historical data
let history = client.read_history(
"00-00-00-00",
now!() - Duration::days(1),
now!(),
).await?;
IEC 61970 / 61968 (CIM)
Grid model exchange standards, supporting import/export of CIM/XML, CIM/JSON:
use eneros_protocol_cim::{CimImporter, CimExporter, CimFormat};
// Import CIM model
let importer = CimImporter::new()
.format(CimFormat::Xml)
.validate_schema(true);
let network = importer.import_file("data/ieee14_cim.xml")?;
println!("Imported bus count: {}", network.bus_count());
println!("Imported branch count: {}", network.branch_count());
// Export to CIM/JSON
let exporter = CimExporter::new()
.format(CimFormat::Json)
.profile(CimProfile::Equipment);
let json = exporter.export_to_string(&network)?;
std::fs::write("data/network_cim.json", json)?;
MQTT 5.0
IoT device communication protocol, EnerOS has a built-in MQTT broker:
# EnerOS built-in MQTT broker, default port 1883
enerosctl mqtt status
enerosctl mqtt subscribe "grid/+/voltage"
enerosctl mqtt publish "grid/bus_1/voltage" "1.024"
use eneros_protocol_mqtt::{MqttClient, QoS};
let mut client = MqttClient::connect("mqtt://localhost:1883")?
.client_id("eneros-agent-1")
.username("agent")
.password("secret");
// Subscribe to topic
client.subscribe("grid/+/voltage", QoS::AtLeastOnce).await?;
// Publish message
client.publish("grid/bus_1/voltage", "1.024", QoS::AtLeastOnce).await?;
// Receive messages
client.on_message(|topic, payload| {
println!("Topic: {}, Payload: {}", topic, String::from_utf8_lossy(payload));
}).await?;
Open API
API Type Comparison
EnerOS simultaneously supports four API types for different scenarios:
| API Type | Protocol | Data Format | Communication Mode | Applicable Scenario | Performance |
|---|---|---|---|---|---|
| REST | HTTP/1.1, HTTP/2 | JSON | Request-response | Management operations, queries | Medium |
| GraphQL | HTTP | JSON | Request-response | Flexible queries | Medium |
| WebSocket | WS | JSON/Binary | Bidirectional stream | Real-time interaction | High |
| SSE | HTTP | Text stream | Server push | Event subscription | High |
| gRPC | HTTP/2 | Protobuf | Bidirectional stream | High-performance RPC | Very high |
REST API
REST API is suitable for management operations and simple queries:
# Query network list
curl http://localhost:8080/api/v1/networks
# Query specific network
curl http://localhost:8080/api/v1/networks/ieee14
# Create Agent
curl -X POST http://localhost:8080/api/v1/agents \
-H "Content-Type: application/json" \
-d '{
"type": "dispatch",
"name": "dispatch-east-1",
"binding": {
"node_id": 1,
"permissions": ["read", "dispatch"]
}
}'
# Query time-series data
curl "http://localhost:8080/api/v1/timeseries/bus_1_voltage?start=2026-07-05T00:00:00Z&end=2026-07-06T00:00:00Z&aggregation=avg&interval=15m"
# Control device
curl -X POST http://localhost:8080/api/v1/devices/breaker_1/control \
-H "Content-Type: application/json" \
-d '{"action": "open"}'
REST response structure example:
{
"code": 0,
"message": "success",
"data": {
"network": {
"id": "ieee14",
"name": "IEEE 14-bus",
"bus_count": 14,
"branch_count": 20,
"buses": [
{"id": 1, "type": "slack", "voltage_pu": 1.06, "angle_deg": 0.0},
{"id": 2, "type": "pv", "voltage_pu": 1.045, "angle_deg": -4.98}
]
}
},
"timestamp": "2026-07-06T10:30:00Z"
}
GraphQL
GraphQL is suitable for flexible queries, where clients can fetch fields on demand:
query {
network(id: "ieee14") {
id
name
buses {
id
type
voltage {
magnitude_pu
angle_deg
}
load {
p_mw
q_mvar
}
}
branches {
id
from
to
power {
p_mw
q_mvar
}
}
}
}
// Rust client calling GraphQL
use eneros_client::GraphQLClient;
let client = GraphQLClient::new("http://localhost:8080/graphql")?;
let query = r#"
query NetworkState($id: ID!) {
network(id: $id) {
buses { id voltage { magnitude_pu } }
}
}
"#;
let result: NetworkResponse = client.query(query, json!({"id": "ieee14"})).await?;
WebSocket
WebSocket is suitable for real-time bidirectional communication:
// Browser-side JavaScript
const ws = new WebSocket('ws://localhost:8080/ws/events');
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'subscribe',
topics: ['grid.voltage', 'grid.frequency', 'agent.status']
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Event:', data);
switch (data.type) {
case 'voltage_change':
updateVoltageChart(data.bus_id, data.value);
break;
case 'fault':
showAlert('Fault alert: ' + data.message);
break;
}
};
// Rust client
use eneros_client::WebSocketClient;
let mut ws = WebSocketClient::connect("ws://localhost:8080/ws/events").await?;
ws.subscribe(vec!["grid.voltage", "grid.frequency"]).await?;
while let Some(event) = ws.recv().await {
println!("Event: {:?}", event);
}
SSE (Server-Sent Events)
SSE is suitable for server-pushed event subscriptions:
const eventSource = new EventSource('http://localhost:8080/api/v1/events');
eventSource.addEventListener('voltage', (e) => {
const data = JSON.parse(e.data);
console.log('Voltage event:', data);
});
eventSource.addEventListener('fault', (e) => {
const data = JSON.parse(e.data);
console.log('Fault event:', data);
});
gRPC
gRPC is suitable for high-performance RPC scenarios:
// proto/eneros.proto
syntax = "proto3";
service PowerFlowService {
rpc Solve(SolveRequest) returns (SolveResponse);
rpc StreamSolve(stream SolveRequest) returns (stream SolveResponse);
}
message SolveRequest {
string network_id = 1;
string method = 2;
uint32 max_iter = 3;
double tolerance = 4;
}
message SolveResponse {
bool converged = 1;
uint32 iterations = 2;
repeated BusResult buses = 3;
}
use eneros_client::grpc::PowerFlowClient;
let mut client = PowerFlowClient::connect("http://localhost:50051").await?;
let response = client.solve(tonic::Request::new(SolveRequest {
network_id: "ieee14".into(),
method: "newton_raphson".into(),
max_iter: 20,
tolerance: 1e-6,
})).await?;
println!("Converged: {}, Iterations: {}",
response.get_ref().converged,
response.get_ref().iterations);
Data Exchange Formats
Supported Formats
| Format | Applicable Scenario | Advantages | Disadvantages |
|---|---|---|---|
| JSON | General API | High universality | Large size |
| XML | CIM model | Standard compatible | Verbose |
| Protobuf | gRPC | Compact and efficient | Requires schema |
| CSV | Data export | Easy to read and process | No types |
| Parquet | Big data | Columnar compression | Binary |
| CBOR | IoT | Compact binary | Few tools |
use eneros_protocol::{Format, Exporter};
// Export to different formats
let exporter = Exporter::new(&network);
// JSON
exporter.to_json("data/network.json")?;
// CIM/XML
exporter.to_cim_xml("data/network_cim.xml")?;
// CSV
exporter.to_csv("data/network.csv")?;
// Parquet
exporter.to_parquet("data/network.parquet")?;
// Import
let network = Importer::from_json("data/network.json")?;
let network = Importer::from_cim_xml("data/network_cim.xml")?;
Integration with SCADA/EMS/DMS
Integration with SCADA
use eneros_protocol_iec104::Iec104Server;
// EnerOS acts as 104 server, polled by SCADA master station
let server = Iec104Server::bind("0.0.0.0:2404")?;
server.on_interrogation(move |common_addr| {
// Return all current telemetry/signal data
let measurements = network.get_all_measurements();
measurements
}).await?;
server.on_command(move |asdu| {
// Receive control commands from SCADA
let command = translate_asdu_to_command(asdu);
gateway.execute(command).await
}).await?;
server.start().await?;
Integration with EMS
use eneros_protocol_cim::{CimImporter, CimExporter};
// Import grid model from EMS (CIM/XML)
let importer = CimImporter::new()
.format(CimFormat::Xml)
.profile(CimProfile::Equipment)
.validate_schema(true);
let network = importer.import_file("ems_export.xml")?;
// Export EnerOS state back to EMS
let exporter = CimExporter::new()
.format(CimFormat::Xml)
.profile(CimProfile::StateVariables);
let state_xml = exporter.export_to_string(&network)?;
// Push back to EMS
ems_client.upload_state(state_xml).await?;
Integration with DMS
// Integration with distribution automation system
use eneros_protocol_dnp3::Dnp3Server;
let server = Dnp3Server::bind_tcp("0.0.0.0:20000")?;
server.on_read_analog(move |start, count| {
// DMS reads analog values
network.read_analog_range(start, count)
}).await?;
server.on_read_binary(move |start, count| {
// DMS reads digital values
network.read_binary_range(start, count)
}).await?;
server.on_control(move |command| {
// DMS sends control commands
let cmd = translate_dnp3_command(command);
gateway.execute(cmd).await
}).await?;
server.start().await?;
Plugin Framework
EnerOS supports custom plugins to extend protocol adaptation:
use eneros_plugin::{ProtocolAdapter, PluginContext, PluginResult};
#[eneros_plugin]
pub struct MyProtocolAdapter {
name: String,
}
impl ProtocolAdapter for MyProtocolAdapter {
fn name(&self) -> &str { &self.name }
fn connect(&mut self, endpoint: &str) -> PluginResult<()> {
// Custom connection logic
Ok(())
}
fn read(&mut self, address: &str) -> PluginResult<Vec<u8>> {
// Custom read logic
Ok(vec![])
}
fn write(&mut self, address: &str, value: &[u8]) -> PluginResult<()> {
// Custom write logic
Ok(())
}
}
// Plugin metadata
eneros_plugin::register_plugin! {
MyProtocolAdapter,
name: "my-protocol",
version: "1.0.0",
author: "EnerOS Team",
description: "Custom protocol adapter",
}
Plugin Security
| Security Mechanism | Description |
|---|---|
| Ed25519 signature verification | Plugins must be signed, verified at startup |
| seccomp sandbox | Restrict system calls, prevent malicious behavior |
| Resource limits | CPU, memory, IO limits |
| Hot load/unload | Dynamic loading at runtime, no restart required |
use eneros_plugin::{PluginManager, PluginConfig, SecurityPolicy};
let manager = PluginManager::new(PluginConfig {
plugin_dir: "plugins/",
security_policy: SecurityPolicy::Strict,
signature_required: true, // Mandatory signature verification
seccomp_sandbox: true, // seccomp sandbox
memory_limit_mb: 128, // Memory limit
cpu_limit_percent: 10, // CPU limit
});
// Load signature-verified plugin
manager.load("my-protocol-1.0.0.plugin").await?;
// Hot unload
manager.unload("my-protocol").await?;
API Performance Comparison
| API Type | Latency | Throughput | Concurrent Connections | Applicable Scenario |
|---|---|---|---|---|
| REST | 5-50ms | 10k QPS | 1000 | Management operations |
| GraphQL | 5-50ms | 5k QPS | 1000 | Flexible queries |
| WebSocket | < 1ms | 100k messages/sec | 10000 | Real-time interaction |
| SSE | < 1ms | 50k events/sec | 5000 | Event push |
| gRPC | 1-5ms | 50k QPS | 5000 | High-performance RPC |
Complete Integration Example
use eneros_os::EnerOS;
use eneros_protocol_iec104::Iec104Server;
use eneros_protocol_iec61850::IedServer;
use eneros_protocol_mqtt::MqttBroker;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Start EnerOS kernel
let eneros = EnerOS::new("config/eneros.toml")?;
eneros.start().await?;
// 2. Start IEC 104 server (SCADA integration)
let iec104 = Iec104Server::bind("0.0.0.0:2404")?;
iec104.start_with_eneros(&eneros).await?;
// 3. Start IEC 61850 server (substation automation)
let iec61850 = IedServer::bind("0.0.0.0:102")?;
iec61850.start_with_eneros(&eneros).await?;
// 4. Start MQTT broker (IoT device access)
let mqtt = MqttBroker::bind("0.0.0.0:1883")?;
mqtt.start_with_eneros(&eneros).await?;
// 5. Start REST API server
eneros.api_server()
.rest("0.0.0.0:8080")
.graphql("0.0.0.0:8080/graphql")
.websocket("0.0.0.0:8080/ws")
.grpc("0.0.0.0:50051")
.start().await?;
// 6. Load custom protocol plugin
eneros.plugin_manager()
.load("plugins/custom-protocol.plugin").await?;
println!("EnerOS started, listening on:");
println!(" - REST API: http://localhost:8080");
println!(" - GraphQL: http://localhost:8080/graphql");
println!(" - WebSocket: ws://localhost:8080/ws");
println!(" - gRPC: localhost:50051");
println!(" - IEC 104: localhost:2404");
println!(" - IEC 61850: localhost:102");
println!(" - MQTT: localhost:1883");
Ok(())
}
Limitations and Trade-offs
| Trade-off | Description | Mitigation Strategy |
|---|---|---|
| Protocol adaptation overhead | Each protocol needs independent adaptation | Built-in mainstream protocols, plugin extension |
| Data model conversion | Different protocols have different data models | Unified mapping to CIM |
| Performance overhead | Protocol conversion adds latency | Kernel-mode direct conversion |
| Compatibility testing | Many protocol versions | Provides compatibility test suite |
| Security risk | Open interfaces increase attack surface | mTLS + signature verification |
Next Steps
- Power-Native First - Power-Native First design philosophy
- IoT Ubiquitous Access - Protocol adaptation details
- API Reference - Complete open API documentation
- Safety Guard - Interoperability security mechanisms