Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,316 changes: 1,310 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

70 changes: 28 additions & 42 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,54 +1,40 @@
[package]
name = "s2energy"
version = "0.3.0"
edition = "2024"
authors = ["Wester Coenraads <wester.coenraads@tno.nl>"]
license = "Apache-2.0"
description = "Provides type definitions and utilities for the S2 energy flexibility standard"
homepage = "https://s2standard.org"
repository = "https://github.com/flexiblepower/s2-rust"
categories = ["api-bindings"]
keywords = ["s2", "energy", "energy-management", "protocol", "automation"]
[workspace]
members = ["s2energy-common", "s2energy-connection", "s2energy-messaging"]
resolver = "3"

[features]
default = ["websockets-json", "dbus"]
websockets-json = ["dep:futures-util", "dep:tokio", "dep:tokio-tungstenite", "dep:serde_json"]
dbus = []

[dependencies]
[workspace.dependencies]
axum = "0.8.8"
axum-extra = { version = "0.12.5", features = ["typed-header"] }
base64 = "0.22.1"
bon = "3.8.0"
chrono = { version = "0.4.42", features = ["serde"] }
regress = "0.10.4" # This dependency is not used directly but necessary for typify to work
uuid = { version = "1.18.1", features = ["v4", "serde"] }
serde = { version = "1.0.228", features = ["derive"] }
futures-util = "0.3.31"
hmac = "0.12.1"
rand = "0.9.2"
regress = "0.10.4"
reqwest = { version = "0.13.1", features = ["json"] }
rustls = "0.23.36"
rustls-platform-verifier = "0.6.2"
semver = "1.0.27"
bon = "3.8.0"
tracing = "0.1.43"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
sha2 = "0.10.9"
thiserror = "2.0.17"
tokio = { version = "1.47.1", features = ["net"] }
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots"] }
tracing = "0.1.43"
uuid = { version = "1.18.1", features = ["v4", "serde"] }

# feature=websockets-json
futures-util = { version = "0.3.31", optional = true }
tokio = { version = "1.47.1", features = ["net"], optional = true }
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-webpki-roots"], optional = true }
serde_json = { version = "1.0.145", optional = true }

[build-dependencies]
# Buildtime
quote = "1.0.41"
prettyplease = "0.2.37"
schemars = "0.8.22"
syn = { version = "2.0.106", features = ["fold"] }
typify = "0.5.0"
serde_json = "1.0.145"
quote = "1.0.41"

[dev-dependencies]
# Development
eyre = "0.6.12"
serde_json = "1.0.145"

# docs.rs-specific configuration
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs_s2energy"]
axum-server = { version = "0.8.0", features = ["tls-rustls"] }

# We can't use cfg(docsrs), because several dependencies have issues when passing
# --cfg docsrs as a compiler arg. So here we define our own docsrs cfg.
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_s2energy)'] }
# Internal
s2energy-common = { path = "./s2energy-common", version = "0.1.0" }
7 changes: 7 additions & 0 deletions s2energy-common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "s2energy-common"
version = "0.1.0"
edition = "2024"

[dependencies]
serde.workspace = true
36 changes: 36 additions & 0 deletions s2energy-common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Abstractions over transport protocols, and their implementations.
//!
//! The S2 specification does not mandate a specific transport protocol. This module provides the primary way
//! transport protocols are abstracted in this crate: [`S2Transport`]. Implementing this trait for your
//! desired transport protocol allows you to use all of the connection types in this library (like [`S2Connection`](crate::connection::S2Connection))
//! with that transport protocol.
//!
//! In addition, this module provides specific transport protocol implementations in its submodules.
//! The most relevant of these is [`websockets_json`], which provides an implementation for JSON over
//! WebSockets according to [the official JSON schema](https://github.com/flexiblepower/s2-ws-json).
//! This is currently the most common and well-supported way to use S2.

use std::error::Error;

use serde::{Serialize, de::DeserializeOwned};

/// Trait used to abstract the underlying transport protocol.
///
/// **End-users are not expected to use this trait directly.** Instead, libraries can implement this trait to provide additional
/// transport protocols that can be used to talk S2 over.
pub trait S2Transport {
/// Error type for errors occurring at a transport level.
type TransportError: Error;

/// Send an S2 message.
fn send(&mut self, message: impl Serialize + Send) -> impl Future<Output = Result<(), Self::TransportError>> + Send;

/// Recceive an S2 message.
fn receive<Message: DeserializeOwned + Send>(&mut self) -> impl Future<Output = Result<Message, Self::TransportError>> + Send;

/// Disconnect this connection.
///
/// This should do whatever is appropriate for the implemented transport protocol. This may include sending
/// e.g. a close frame. When the future resolves, the connection should be fully terminated.
fn disconnect(self) -> impl Future<Output = ()> + Send;
}
22 changes: 22 additions & 0 deletions s2energy-connection/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "s2energy-connection"
version = "0.1.0"
edition = "2024"

[dependencies]
axum.workspace = true
axum-extra.workspace = true
base64.workspace = true
hmac.workspace = true
rand.workspace = true
reqwest.workspace = true
rustls.workspace = true
rustls-platform-verifier.workspace = true
serde.workspace = true
sha2.workspace = true
thiserror.workspace = true
tokio.workspace = true
uuid.workspace = true

[dev-dependencies]
axum-server.workspace = true
74 changes: 74 additions & 0 deletions s2energy-connection/examples/communication-client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use std::{convert::Infallible, path::PathBuf, sync::Arc};
use uuid::uuid;

use rustls::pki_types::{CertificateDer, pem::PemObject};
use s2energy_connection::{
AccessToken, MessageVersion, S2NodeId,
communication::{Client, ClientConfig, ClientPairing, NodeConfig},
};

struct MemoryPairing {
communication_url: String,
tokens: Vec<AccessToken>,
server: S2NodeId,
client: S2NodeId,
}

impl ClientPairing for &mut MemoryPairing {
type Error = Infallible;

fn client_id(&self) -> S2NodeId {
self.client.clone()
}

fn server_id(&self) -> S2NodeId {
self.server.clone()
}

fn access_tokens(&self) -> impl AsRef<[AccessToken]> {
&self.tokens
}

fn communication_url(&self) -> impl AsRef<str> {
&self.communication_url
}

async fn set_access_tokens(&mut self, tokens: Vec<AccessToken>) -> Result<(), Self::Error> {
self.tokens = tokens;
Ok(())
}
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
let client = Client::new(
ClientConfig {
additional_certificates: vec![
CertificateDer::from_pem_file(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join("root.pem")).unwrap(),
],
endpoint_description: None,
},
Arc::new(NodeConfig::builder(vec![MessageVersion("v1".into())]).build()),
);

let mut pairing = MemoryPairing {
communication_url: "https://localhost:8005/".into(),
tokens: vec![AccessToken("0123456789ABCDEF".into())],
server: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8").into(),
client: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c7").into(),
};

let connection_info = client.connect(&mut pairing).await.unwrap();

println!(
"Url: {}, token: {}",
connection_info.communication_url, connection_info.communication_token.0
);

let connection_info = client.connect(&mut pairing).await.unwrap();

println!(
"Url: {}, token: {}",
connection_info.communication_url, connection_info.communication_token.0
);
}
108 changes: 108 additions & 0 deletions s2energy-connection/examples/communication-server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use std::{
convert::Infallible,
net::SocketAddr,
path::PathBuf,
sync::{Arc, Mutex},
};
use uuid::uuid;

use axum_server::tls_rustls::RustlsConfig;
use s2energy_connection::{
AccessToken, MessageVersion, S2NodeId,
communication::{NodeConfig, PairingLookupResult, Server, ServerConfig, ServerPairing, ServerPairingStore},
};

struct MemoryPairingStoreInner {
token: AccessToken,
config: Arc<NodeConfig>,
server: S2NodeId,
client: S2NodeId,
}

#[derive(Clone)]
struct MemoryPairingStore(Arc<Mutex<MemoryPairingStoreInner>>);

impl MemoryPairingStore {
fn new() -> Self {
MemoryPairingStore(Arc::new(Mutex::new(MemoryPairingStoreInner {
token: AccessToken("0123456789ABCDEF".into()),
config: Arc::new(NodeConfig::builder(vec![MessageVersion("v1".into())]).build()),
server: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8").into(),
client: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c7").into(),
})))
}
}

impl ServerPairingStore for MemoryPairingStore {
type Error = Infallible;

type Pairing<'a>
= MemoryPairingStore
where
Self: 'a;

async fn lookup(
&self,
request: s2energy_connection::communication::PairingLookup,
) -> Result<s2energy_connection::communication::PairingLookupResult<Self::Pairing<'_>>, Self::Error> {
let this = self.0.lock().unwrap();
if this.client == request.client && this.server == request.server {
Ok(PairingLookupResult::Pairing(self.clone()))
} else {
Ok(PairingLookupResult::NeverPaired)
}
}
}

impl ServerPairing for MemoryPairingStore {
type Error = Infallible;

fn access_token(&self) -> impl AsRef<AccessToken> {
self.0.lock().unwrap().token.clone()
}

fn config(&self) -> impl AsRef<NodeConfig> {
self.0.lock().unwrap().config.clone()
}

async fn set_access_token(&mut self, token: AccessToken) -> Result<(), Self::Error> {
self.0.lock().unwrap().token = token;
Ok(())
}

async fn update_remote_node_description(&mut self, _node_description: s2energy_connection::S2NodeDescription) {
println!("Received updated node description");
}

async fn update_remote_endpoint_description(&mut self, _endpoint_description: s2energy_connection::S2EndpointDescription) {
println!("Received updated endpoint description");
}
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
let server = Server::new(
ServerConfig {
base_url: "localhost".into(),
endpoint_description: None,
},
MemoryPairingStore::new(),
);

let addr = SocketAddr::from(([127, 0, 0, 1], 8005));

let rustls_config = RustlsConfig::from_pem_file(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("testdata")
.join("localhost.chain.pem"),
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join("localhost.key"),
)
.await
.unwrap();

println!("listening on http://{}", addr);
axum_server::bind_rustls(addr, rustls_config)
.serve(server.get_router().into_make_service())
.await
.unwrap();
}
55 changes: 55 additions & 0 deletions s2energy-connection/examples/pairing-client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use std::sync::Arc;
use uuid::uuid;

use s2energy_connection::{
Deployment, MessageVersion, S2NodeDescription, S2Role,
pairing::{Client, ClientConfig, EndpointConfig, PairingRemote},
};

const PAIRING_TOKEN: &[u8] = &[1, 2, 3];

#[tokio::main(flavor = "current_thread")]
async fn main() {
let config = EndpointConfig::builder(
S2NodeDescription {
id: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c7").into(),
brand: String::from("super-reliable-corp"),
logo_uri: None,
type_: String::from("fancy"),
model_name: String::from("the best"),
user_defined_name: None,
role: S2Role::Rm,
},
vec![MessageVersion("v1".into())],
)
.with_connection_initiate_url("client.example.com".into())
.build()
.unwrap();

let client = Client::new(
Arc::new(config),
ClientConfig {
additional_certificates: vec![],
pairing_deployment: Deployment::Lan,
},
)
.unwrap();

let pair_result = client
.pair(
PairingRemote {
url: "https://test.local:8005".into(),
id: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8").into(),
},
PAIRING_TOKEN,
)
.await
.unwrap();

match pair_result.role {
s2energy_connection::pairing::PairingRole::CommunicationClient { initiate_url } => {
println!("Paired as client, url: {initiate_url}, token: {}", pair_result.token.0)
}
s2energy_connection::pairing::PairingRole::CommunicationServer => println!("Paired as server, token: {}", pair_result.token.0),
}
}
Loading