//! 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, repository: Arc, } 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, repository: Arc, ) -> 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 { 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); } }