ocppsim 0.2.0

A terminal OCPP-J charge point simulator.
Documentation
use std::time::Duration;

use serde_json::{Value, json};
use tokio::sync::mpsc;

use super::{
  ConfigurationKey, ConnectorStatus, OcppVersion, PendingCall, PendingContext,
  ResponseStatus, SecurityProfileFallback, Simulator, SimulatorConfig,
  TxEventType, UiEvent, UiLogLevel, now_timestamp,
  validate_negotiated_subprotocol,
};

mod charging_profiles;
mod configuration;
mod schema_validation;
mod security;
mod state;
mod transactions;
mod ws_lifecycle;

fn simulator_test_config(protocol: OcppVersion) -> SimulatorConfig {
  SimulatorConfig {
    ws_url: "ws://localhost:9000/ocpp".to_string(),
    cp_id: "CP-TEST".to_string(),
    protocol,
    connectors: 2,
    vendor: "ocppsim".to_string(),
    model: "test".to_string(),
    firmware: "0.0.0".to_string(),
    append_cp_id: false,
    trace_frames: false,
    strict: false,
    security_profile: None,
    basic_auth_password: None,
    ca_cert_path: None,
    client_cert_path: None,
    client_key_path: None,
    request_timeout: Duration::from_secs(30),
    heartbeat_seconds: Some(10),
  }
}

fn simulator_for_tests_with_protocol(protocol: OcppVersion) -> Simulator {
  let (simulator, _ui_rx) = simulator_for_tests_with_protocol_and_ui(protocol);
  simulator
}

fn simulator_for_tests_with_protocol_and_ui(
  protocol: OcppVersion,
) -> (Simulator, mpsc::UnboundedReceiver<UiEvent>) {
  let config = simulator_test_config(protocol);
  let (ui_tx, ui_rx) = mpsc::unbounded_channel();
  let (cmd_tx, _cmd_rx) = mpsc::unbounded_channel();
  (Simulator::new(config, ui_tx, cmd_tx), ui_rx)
}

fn simulator_for_tests() -> Simulator {
  simulator_for_tests_with_protocol(OcppVersion::V1_6)
}

fn v2_x_protocols() -> [OcppVersion; 2] {
  [OcppVersion::V2_0_1, OcppVersion::V2_1]
}

fn for_each_v2_x_simulator(
  mut assert_case: impl FnMut(OcppVersion, Simulator),
) {
  for protocol in v2_x_protocols() {
    assert_case(protocol, simulator_for_tests_with_protocol(protocol));
  }
}

fn v2_x_schema_dir(protocol: OcppVersion) -> &'static str {
  match protocol {
    OcppVersion::V2_0_1 => "schemas/2.0.1",
    OcppVersion::V2_1 => "schemas/2.1",
    OcppVersion::V1_6 => panic!("expected v2.x protocol"),
  }
}

fn assert_schema_valid(relative_schema: &str, payload: &Value) {
  let schema_text = crate::embedded_schemas::schema_text(relative_schema)
    .unwrap_or_else(|| panic!("missing embedded schema {relative_schema}"));
  let schema: Value = serde_json::from_str(schema_text).expect("schema");
  let validator = jsonschema::validator_for(&schema).expect("compile schema");
  let errors = validator
    .iter_errors(payload)
    .map(|error| error.to_string())
    .collect::<Vec<_>>();
  assert!(
    errors.is_empty(),
    "payload did not match {}: {}\npayload={}",
    relative_schema,
    errors.join("; "),
    payload
  );
}

fn queued_payload(simulator: &Simulator, action: &str) -> Value {
  simulator
    .queue
    .iter()
    .find(|call| call.action == action)
    .map(|call| call.payload.clone())
    .expect("queued action")
}

fn schema_path(schema_dir: &str, file_name: &str) -> String {
  format!("{schema_dir}/{file_name}")
}

fn get_variable_data(variable: &str) -> Value {
  json!({
    "component": { "name": "ChargingStation" },
    "variable": { "name": variable },
  })
}

fn set_variable_data(variable: &str, value: &str) -> Value {
  json!({
    "component": { "name": "ChargingStation" },
    "variable": { "name": variable },
    "attributeValue": value,
  })
}