s2energy 0.3.0

Provides type definitions and utilities for the S2 energy flexibility standard
Documentation
//! This crate provides type definitions and utilities for working with the [S2 energy flexibility standard](https://s2standard.org). S2 is a communication standard for energy flexibility and energy management in homes and buildings, designed to simplify the use of energy flexibility of smart devices. To learn more about the S2 standard:
//! - [Read the documentation](https://docs.s2standard.org/) for a detailed explanation of S2
//! - [Visit the website](https://s2standard.org) for a high-level explanation of S2
//! - [Read the whitepaper](https://ecostandard.org/wp-content/uploads/2024/05/20240521_DSF_PositionPaper.pdf) to learn why it's important to expose and utilise energy flexibility
//!
//! # Crate contents
//! This crate provides Rust types for all types specified by S2. It also includes provides utilities in the [`connection`] and [`transport`] modules
//! that help you set up S2 connections.
//!
//! JSON over WebSockets is a common and recommended way to implement S2, but you're free to choose a different
//! format and communication protocol. In that case, the types in this crate should still be useful but you may wish to disable the `websockets-json` feature.
//!
//! # Using this crate
//! When implementing S2, you need to have a clear picture of which control types you want to implement. If you're not sure which control type
//! you're implementing, see [the documentation website](https://docs.s2standard.org/docs/concepts/control-types/) for an overview of the
//! available control types.
//!
//! S2 types common to all control types (such as [`PowerValue`](common::PowerValue) and [`Commodity`](common::Commodity)) are in the [`common`] module.
//! The types specific to control types (such as [`FRBC.ActuatorStatus`](frbc::ActuatorStatus) or [`PEBC.PowerConstraints`](pebc::PowerConstraints)) are
//! divided into modules based on the control type they belong to.
//!
//! ### Creating  S2 types
//! S2 types have all their fields exposed, so you can construct them using regular Rust constructor syntax:
//! ```
//! # use s2energy::common::Id;
//! # let actuator_id = Id::generate();
//! # let operation_mode_id = Id::generate();
//! use s2energy::{common::NumberRange, frbc::ActuatorStatus};
//!
//! let range = NumberRange {
//!     start_of_range: 1.0,
//!     end_of_range: 30.5,
//! };
//!
//! let actuator_status = ActuatorStatus::builder()
//!     .active_operation_mode_id(operation_mode_id)
//!     .actuator_id(actuator_id)
//!     .operation_mode_factor(0.7)
//!     .build();
//! ```
//!
//! Most S2 types have an automatically generated builder (such as [`ActuatorStatus::builder`](frbc::ActuatorStatus::builder) in the above example), so
//! you can use those to create an actuator status as a more convenient alternative to the regular object initialization syntax.
//! S2 types with only one field have a `new` function to easily create them.
//!
//! ### Working with [`Message`](common::Message)
//! When sending or receiving S2 messages, you'll be working with [`common::Message`]. This type represents all possible S2 messages in a big enum. When
//! receiving messages, you'll want to match on `Message` to determine how to handle it:
//! ```
//! # use s2energy::common::Message;
//! # use s2energy::frbc::StorageStatus;
//! # let incoming_message = Message::FrbcStorageStatus(StorageStatus::new(2.1));
//! match incoming_message {
//!     Message::FrbcSystemDescription(system_description) => { /* Handle it */ },
//!     Message::FrbcStorageStatus(storage_status) => { /* Handle it */ },
//!     _ => { /* Ignore other messages */ }
//! }
//! ```
//!
//! All types that serve as the content of a message (such as [`frbc::SystemDescription`] and [`frbc::StorageStatus`] in the above example) implement
//! `Into<Message>` for convenience. This means you can do:
//! ```
//! # use s2energy::common::Message;
//! # use s2energy::frbc::StorageStatus;
//! let storage_status = StorageStatus::new(2.1);
//! let message: Message = storage_status.into();
//! ```
//! [`Connection::send_message`](connection::S2Connection::send_message) accepts an `impl Into<Message>`, so you can just give it any compatible
//! type and it will work.
//! 
//! ### Sending/receiving S2 messages
//! S2 does not specify a particular transport protocol for S2 messages. As a result, many transport protocols can be used: WebSockets, MQTT, [even D-Bus](https://github.com/victronenergy/venus/wiki/Venus-OS-D%E2%80%90Bus-S2-Interface).
//! To facilitate the use of different transport protocols, this crates provides a central abstraction in [`connection::S2Connection`] and [`transport::S2Transport`].
//! An `S2Connection` can use any transport protocol implementing the `S2Transport` trait.
//! 
//! This crate provides some transport implementations for end-users. Currently, only a WebSockets implementation is provided (in [`transport::websockets_json`]).
//! D-Bus support is also planned for the near future.
//!
//! ### Crate features
//! The crate currently has these features:
//! - `websockets-json` (enabled by default): enables WebSocket support with the [`transport::websockets_json`] module.
//!
//! # Reading this documentation
//! Part of this documentation is automatically generated by extracting descriptions from the S2 specification.
//! Specifically, the S2 type definitions in [`common`] and the control type specific modules are all generated, and their documentation
//! can be spotty. The [language-agnostic documentation for the S2 standard](https://docs.s2standard.org/)
//! is often more helpful and complete.
//!
//! Module documentation (for all modules) and documentation for other types (like those in [`connection`] or [`transport`]) is hand-written and generally of a higher standard.
//! It assumes that you are familiar with the S2 standard; if this is not the case, it may be useful to refer to [the S2 documentation website](https://docs.s2standard.org/docs/welcome/).
#![warn(missing_docs)]
#![cfg_attr(docsrs_s2energy, feature(doc_cfg))]

include!(concat!(env!("OUT_DIR"), "/generated.rs"));

pub mod connection;
pub mod transport;

#[cfg(test)]
mod test {
    use crate::{
        common::{Id, Message},
        frbc,
    };
    use std::str::FromStr;

    // Reason for test: a simple serialization/deserialization roundtrip test.
    #[test]
    fn actuator_status_json() -> eyre::Result<()> {
        let actuator_status = frbc::ActuatorStatus {
            active_operation_mode_id: Id::from_str("862acd86-cfd5-4a5c-9ad8-d28a5d192f03").expect("Error parsing ID"),
            actuator_id: Id::from_str("19e02398-3b5a-4f42-bd64-7929bce7369f").expect("Error parsing ID"),
            message_id: Id::from_str("d147a031-f0bc-4fb6-9713-5a3bd5027481").expect("Error parsing ID"),
            operation_mode_factor: 0.64,
            previous_operation_mode_id: None,
            transition_timestamp: Some(chrono::DateTime::from_timestamp(1736256315, 0).expect("Error parsing timestamp")),
        };
        let message = Message::FrbcActuatorStatus(actuator_status);
        let serialized = serde_json::to_string(&message)?;
        let expected_serialized = r#"{"message_type":"FRBC.ActuatorStatus","active_operation_mode_id":"862acd86-cfd5-4a5c-9ad8-d28a5d192f03","actuator_id":"19e02398-3b5a-4f42-bd64-7929bce7369f","message_id":"d147a031-f0bc-4fb6-9713-5a3bd5027481","operation_mode_factor":0.64,"transition_timestamp":"2025-01-07T13:25:15Z"}"#;
        assert_eq!(serialized, expected_serialized);

        let roundtripped: Message = serde_json::from_str(&serialized)?;
        assert_eq!(roundtripped, message);

        Ok(())
    }

    // Reason for test: the schema version is manually typed, and `s2_schema_version` can panic if a mistake was made in the version number.
    #[test]
    fn s2_schema_version_doesnt_panic() {
        let _ = crate::s2_schema_version();
    }
}