Skip to main content

Open & Interoperable

Core Concepts

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:

SystemMain ProtocolsData FormatCommunication Mode
Dispatch automation (EMS)IEC 60870-5-104ProprietaryMaster-slave
Substation automationIEC 61850MMS/GOOSEPublish-subscribe
Distribution automation (DMS)IEC 60870-5-104 / DNP3ProprietaryMaster-slave
Telecontrol RTUIEC 60870-5-101/104ProprietaryMaster-slave
Smart metersDL/T 698.45 / ModbusProprietaryMaster-slave
Grid modelIEC 61970 (CIM)XML/RDFFile
Distribution modelIEC 61968 (CIM)XML/JSONFile
Wind/SolarIEC 61400-25MMSPublish-subscribe
IoT devicesMQTT / CoAPJSONPublish-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

ProtocolStandard NumberApplicable ScenarioCommunication ModeCrate
IEC 61850Substation automationSubstation devicesMMS/GOOSE/SVeneros-protocol-iec61850
IEC 60870-5-104Telecontrol protocolDispatch master-substationMaster-slaveeneros-protocol-iec104
IEC 60870-5-101Telecontrol protocol (serial)Legacy RTUMaster-slaveeneros-protocol-iec101
IEC 61970 (CIM)Grid modelEMS model exchangeFileeneros-protocol-cim
IEC 61968 (CIM)Distribution modelDMS model exchangeFileeneros-protocol-cim
DNP3Distribution automationNorth American distributionMaster-slaveeneros-protocol-dnp3
ModbusIndustrial devicesGeneral industrial devicesMaster-slaveeneros-protocol-modbus
IEC 61400-25Wind farmsWind turbine monitoringMMSeneros-protocol-iec61400
DL/T 698.45Smart metersChinese metersMaster-slaveeneros-protocol-dlt698
MQTT 5.0IoTIoT devicesPublish-subscribeeneros-protocol-mqtt
OPC UAIndustrial unified architectureGeneral industrialClient-servereneros-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 TypeProtocolData FormatCommunication ModeApplicable ScenarioPerformance
RESTHTTP/1.1, HTTP/2JSONRequest-responseManagement operations, queriesMedium
GraphQLHTTPJSONRequest-responseFlexible queriesMedium
WebSocketWSJSON/BinaryBidirectional streamReal-time interactionHigh
SSEHTTPText streamServer pushEvent subscriptionHigh
gRPCHTTP/2ProtobufBidirectional streamHigh-performance RPCVery 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

FormatApplicable ScenarioAdvantagesDisadvantages
JSONGeneral APIHigh universalityLarge size
XMLCIM modelStandard compatibleVerbose
ProtobufgRPCCompact and efficientRequires schema
CSVData exportEasy to read and processNo types
ParquetBig dataColumnar compressionBinary
CBORIoTCompact binaryFew 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 MechanismDescription
Ed25519 signature verificationPlugins must be signed, verified at startup
seccomp sandboxRestrict system calls, prevent malicious behavior
Resource limitsCPU, memory, IO limits
Hot load/unloadDynamic 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 TypeLatencyThroughputConcurrent ConnectionsApplicable Scenario
REST5-50ms10k QPS1000Management operations
GraphQL5-50ms5k QPS1000Flexible queries
WebSocket< 1ms100k messages/sec10000Real-time interaction
SSE< 1ms50k events/sec5000Event push
gRPC1-5ms50k QPS5000High-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-offDescriptionMitigation Strategy
Protocol adaptation overheadEach protocol needs independent adaptationBuilt-in mainstream protocols, plugin extension
Data model conversionDifferent protocols have different data modelsUnified mapping to CIM
Performance overheadProtocol conversion adds latencyKernel-mode direct conversion
Compatibility testingMany protocol versionsProvides compatibility test suite
Security riskOpen interfaces increase attack surfacemTLS + signature verification

Next Steps