Files
sta/backend/src/application/use_cases/toggle_relay.rs

208 lines
8.0 KiB
Rust
Raw Normal View History

//! Toggle relay use case.
//!
//! This use case handles toggling a single relay's state from on to off or vice versa.
//! It coordinates with the relay controller to read the current state, toggle it,
//! and write the new state back.
use std::sync::Arc;
use crate::domain::relay::{
controller::{ControllerError, RelayController},
entity::Relay,
repository::{RelayLabelRepository, RepositoryError},
types::RelayId,
};
/// Error type for toggle relay use case operations.
#[derive(Debug, thiserror::Error)]
pub enum ToggleRelayError {
/// Error from the relay controller (connection, timeout, protocol issues).
#[error("Controller error: {0}")]
Controller(#[from] ControllerError),
/// Error from the label repository.
#[error("Repository error: {0}")]
Repository(#[from] RepositoryError),
}
/// Use case for toggling a relay's state.
///
/// This use case:
/// 1. Reads the current state of the specified relay
/// 2. Toggles the state (On → Off, Off → On)
/// 3. Writes the new state to the relay
/// 4. Returns the updated relay entity with its label
///
/// # Example
///
/// ```rust,ignore
/// let use_case = ToggleRelayUseCase::new(controller, repository);
/// let relay = use_case.execute(RelayId::new(1).unwrap()).await?;
/// // relay.state() is now toggled from its previous value
/// ```
pub struct ToggleRelayUseCase {
controller: Arc<dyn RelayController>,
repository: Arc<dyn RelayLabelRepository>,
}
impl ToggleRelayUseCase {
/// Creates a new toggle relay use case.
///
/// # Arguments
///
/// * `controller` - The relay controller for hardware communication
/// * `repository` - The label repository for relay labels
#[must_use]
pub fn new(
controller: Arc<dyn RelayController>,
repository: Arc<dyn RelayLabelRepository>,
) -> Self {
Self {
controller,
repository,
}
}
/// Executes the toggle relay use case.
///
/// Reads the current state, toggles it, writes the new state, and returns
/// the updated relay entity including its label.
///
/// # Arguments
///
/// * `relay_id` - The ID of the relay to toggle (1-8)
///
/// # Returns
///
/// The updated `Relay` entity with the new state.
///
/// # Errors
///
/// Returns `ToggleRelayError` if:
/// - Controller fails to read/write relay state
/// - Repository fails to retrieve the label
pub async fn execute(&self, relay_id: RelayId) -> Result<Relay, ToggleRelayError> {
tracing::debug!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), "Toggling relay state");
let current_state = self.controller.read_relay_state(relay_id).await?;
tracing::debug!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), ?current_state, "Read current state");
let new_state = current_state.toggle();
tracing::debug!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), ?new_state, "New state after toggle");
self.controller
.write_relay_state(relay_id, new_state)
.await?;
tracing::info!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), ?new_state, "Successfully toggled relay");
let label = self.repository.get_label(relay_id).await?;
Ok(Relay::new(relay_id, new_state, label))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::relay::types::RelayState;
use crate::infrastructure::modbus::mock_controller::MockRelayController;
use crate::infrastructure::persistence::label_repository::MockRelayLabelRepository;
/// Helper to create a test controller with initialized relays.
async fn create_test_controller() -> MockRelayController {
let controller = MockRelayController::new();
for i in 1..=8 {
controller
.write_relay_state(RelayId::new(i).unwrap(), RelayState::Off)
.await
.unwrap();
}
controller
}
#[tokio::test]
async fn test_execute_toggles_relay_state_off_to_on() {
let controller = Arc::new(create_test_controller().await);
let repository = Arc::new(MockRelayLabelRepository::new());
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
let relay_id = RelayId::new(1).unwrap();
let initial_state = controller.read_relay_state(relay_id).await.unwrap();
assert_eq!(initial_state, RelayState::Off);
let result = use_case.execute(relay_id).await.unwrap();
assert_eq!(result.state(), RelayState::On);
assert_eq!(result.id(), relay_id);
}
#[tokio::test]
async fn test_execute_toggles_relay_state_on_to_off() {
let controller = Arc::new(create_test_controller().await);
let repository = Arc::new(MockRelayLabelRepository::new());
let relay_id = RelayId::new(1).unwrap();
controller
.write_relay_state(relay_id, RelayState::On)
.await
.unwrap();
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
let result = use_case.execute(relay_id).await.unwrap();
assert_eq!(result.state(), RelayState::Off);
}
#[tokio::test]
async fn test_execute_returns_error_if_controller_fails() {
let controller = Arc::new(MockRelayController::new().with_timeout_simulation());
let repository = Arc::new(MockRelayLabelRepository::new());
let use_case = ToggleRelayUseCase::new(controller, repository);
let relay_id = RelayId::new(1).unwrap();
let result = use_case.execute(relay_id).await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ToggleRelayError::Controller(_)
));
}
#[tokio::test]
async fn test_execute_updates_state_in_controller() {
let controller = Arc::new(create_test_controller().await);
let repository = Arc::new(MockRelayLabelRepository::new());
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
let relay_id = RelayId::new(1).unwrap();
use_case.execute(relay_id).await.unwrap();
let state_in_controller = controller.read_relay_state(relay_id).await.unwrap();
assert_eq!(state_in_controller, RelayState::On);
}
#[tokio::test]
async fn test_execute_returns_relay_with_label() {
let controller = Arc::new(create_test_controller().await);
let repository = Arc::new(MockRelayLabelRepository::new());
let relay_id = RelayId::new(1).unwrap();
let label = crate::domain::relay::types::RelayLabel::new("Test Label".to_string()).unwrap();
repository
.save_label(relay_id, label.clone())
.await
.unwrap();
let use_case = ToggleRelayUseCase::new(controller, repository);
let result = use_case.execute(relay_id).await.unwrap();
assert_eq!(result.label(), Some(label));
}
#[tokio::test]
async fn test_execute_returns_relay_without_label_when_none_set() {
let controller = Arc::new(create_test_controller().await);
let repository = Arc::new(MockRelayLabelRepository::new());
let use_case = ToggleRelayUseCase::new(controller, repository);
let relay_id = RelayId::new(1).unwrap();
let result = use_case.execute(relay_id).await.unwrap();
assert_eq!(result.label(), None);
}
#[tokio::test]
async fn test_execute_double_toggle_returns_to_original_state() {
let controller = Arc::new(create_test_controller().await);
let repository = Arc::new(MockRelayLabelRepository::new());
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
let relay_id = RelayId::new(1).unwrap();
let initial_state = controller.read_relay_state(relay_id).await.unwrap();
assert_eq!(initial_state, RelayState::Off);
use_case.execute(relay_id).await.unwrap();
let result = use_case.execute(relay_id).await.unwrap();
assert_eq!(result.state(), RelayState::Off);
}
}