aquarium_control/thermal/
heating.rs

1/* Copyright 2024 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9
10use log::error;
11
12#[cfg(not(test))]
13use log::warn;
14
15use spin_sleep::SpinSleeper;
16use std::sync::{Arc, Mutex};
17use std::time::{Duration, Instant};
18
19#[cfg(all(target_os = "linux", not(test)))]
20use nix::unistd::gettid;
21
22use crate::database::sql_interface_error::SqlInterfaceError;
23use crate::database::sql_interface_heating_stats::{HeatingStatsEntry, SqlInterfaceHeatingStats};
24use crate::database::thermal_set_value_updater_trait::ThermalSetValueUpdaterTrait;
25use crate::sensors::sensor_manager::SensorManagerSignals;
26use crate::sensors::tank_level_switch::TankLevelSwitchSignals;
27use crate::thermal::heating_channels::HeatingChannels;
28use crate::thermal::heating_config::HeatingConfig;
29use crate::utilities::acknowledge_signal_handler::AcknowledgeSignalHandlerTrait;
30use crate::utilities::channel_content::{ActuatorState, AquariumDevice, InternalCommand};
31use crate::utilities::check_mutex_access_duration::CheckMutexAccessDurationTrait;
32use crate::utilities::common::check_if_mutex_is_blocked;
33use crate::utilities::database_ping_trait::DatabasePingTrait;
34use crate::utilities::logger::log_error_chain;
35use crate::utilities::proc_ext_req::ProcessExternalRequestTrait;
36use crate::utilities::sawtooth_profile::SawToothProfile;
37use crate::utilities::wait_for_termination::WaitForTerminationTrait;
38use crate::{manage_cycle_time_thermal, perform_schedule_check, update_thermal_set_values};
39#[cfg(not(test))]
40use log::info;
41
42const CYCLE_TIME_HEATING_MILLIS: u64 = 500;
43
44const TIME_INCREMENT_HEATING_SECS: f32 = CYCLE_TIME_HEATING_MILLIS as f32 / 1000.0;
45
46/// allow max. 10 milliseconds for mutex to be blocked by any other thread
47const MAX_MUTEX_ACCESS_DURATION_MILLIS: u64 = 10;
48
49/// Trait for the execution of data transfer to SQL database after midnight has arrived.
50/// This trait allows running the main control with a mock implementation for testing.
51pub trait HeatingStatsDataTransferTrait {
52    /// Transfers a completed daily heating statistics entry to the database.
53    ///
54    /// This function is called once per day, typically just after midnight, to persist
55    /// the aggregated heating statistics (energy usage, average temperatures) for the
56    /// previous day.
57    ///
58    /// # Arguments
59    /// * `data` - The `HeatingStatsEntry` struct containing the statistics for the day.
60    /// * `sql_interface_heating` - A mutable reference to the `SqlInterfaceHeatingStats` instance,
61    ///   used for database interaction.
62    ///
63    /// # Returns
64    /// An empty `Result` (`Ok(())`) on a successful database transaction.
65    ///
66    /// # Errors
67    /// Returns an `SqlInterfaceError` if the database operation fails (e.g., connection
68    /// issue, SQL error during insertion/update).
69    fn transfer_heating_stats(
70        &mut self,
71        data: HeatingStatsEntry,
72        sql_interface_heating: &mut SqlInterfaceHeatingStats,
73    ) -> Result<(), SqlInterfaceError>;
74}
75
76/// Sends a command to the relay manager to actuate the heater and waits for a response.
77///
78/// This private helper function encapsulates the logic for sending a command
79/// (either `SwitchOn` or `SwitchOff`) to the relay manager. It handles channel
80/// communication, error logging, and updates the shared actuation mutex.
81///
82/// # Arguments
83/// * `command` - The `InternalCommand` to send (`SwitchOn` or `SwitchOff`).
84/// * `mutex_device_scheduler_heating` - A reference to the shared actuation mutex.
85/// * `mutex_blocked_during_actuation` - A mutable flag to track if the actuation mutex was blocked.
86/// * `heating_channels` - A mutable reference to the struct containing the channels.
87fn actuate_heater(
88    command: InternalCommand,
89    mutex_device_scheduler_heating: &Arc<Mutex<i32>>,
90    mutex_blocked_during_actuation: &mut bool,
91    heating_channels: &mut HeatingChannels,
92) {
93    // reduce the probability of wrong warning
94    *mutex_blocked_during_actuation |= check_if_mutex_is_blocked(mutex_device_scheduler_heating);
95
96    // avoid electrical overloads through parallel device actuation
97    let mut mutex_data = mutex_device_scheduler_heating.lock().unwrap();
98
99    // Use map_err for more concise error handling on the send operation.
100    if let Err(e) = heating_channels.send_to_relay_manager(command.clone()) {
101        let error_message = format!("Channel communication to relay manager for {command} failed.");
102        log_error_chain(module_path!(), &error_message, e);
103    } else {
104        // If send was successful, wait for the response.
105        if let Err(e) = heating_channels.receive_from_relay_manager() {
106            let error_message =
107                format!("Receiving answer from relay manager for {command} failed.");
108            log_error_chain(module_path!(), &error_message, e);
109        }
110    }
111    *mutex_data = mutex_data.saturating_add(1);
112}
113
114/// Commands the relay manager to switch on the heater.
115///
116/// This function is a specific wrapper around `actuate_heater` that sends the `SwitchOn` command.
117///
118/// # Arguments
119/// * `mutex_device_scheduler_heating` - A reference to the shared actuation mutex.
120/// * `mutex_blocked_during_actuation` - A mutable flag to track if the actuation mutex was blocked.
121/// * `heating_channels` - A mutable reference to the struct containing the channels.
122fn switch_on_heater(
123    mutex_device_scheduler_heating: &Arc<Mutex<i32>>,
124    mutex_blocked_during_actuation: &mut bool,
125    heating_channels: &mut HeatingChannels,
126) {
127    actuate_heater(
128        InternalCommand::SwitchOn(AquariumDevice::Heater),
129        mutex_device_scheduler_heating,
130        mutex_blocked_during_actuation,
131        heating_channels,
132    );
133}
134
135/// Commands the relay manager to switch off the heater.
136///
137/// This function is a specific wrapper around `actuate_heater` that sends the `SwitchOff` command.
138///
139/// # Arguments
140/// * `mutex_device_scheduler_heating` - A reference to the shared actuation mutex.
141/// * `mutex_blocked_during_actuation` - A mutable flag to track if the actuation mutex was blocked.
142/// * `heating_channels` - A mutable reference to the struct containing the channels.
143fn switch_off_heater(
144    mutex_device_scheduler_heating: &Arc<Mutex<i32>>,
145    mutex_blocked_during_actuation: &mut bool,
146    heating_channels: &mut HeatingChannels,
147) {
148    actuate_heater(
149        InternalCommand::SwitchOff(AquariumDevice::Heater),
150        mutex_device_scheduler_heating,
151        mutex_blocked_during_actuation,
152        heating_channels,
153    );
154}
155
156/// Contains `Arc<Mutex>`-wrapped shared data points accessed by the heating control.
157///
158/// These mutexes provide thread-safe access to various sensor readings and
159/// device states that are used by the heating control.
160pub struct HeatingMutexes {
161    /// Mutex for signals from sensor manager
162    pub(crate) mutex_sensor_manager_signals: Arc<Mutex<SensorManagerSignals>>,
163
164    /// Mutex for the tank level switch signals
165    pub(crate) mutex_tank_level_switch_signals: Arc<Mutex<TankLevelSwitchSignals>>,
166
167    /// Mutex for the heater status
168    pub(crate) mutex_heating_status: Arc<Mutex<bool>>,
169}
170
171#[cfg_attr(doc, aquamarine::aquamarine)]
172/// Contains the configuration and the implementation for the heating control.
173/// Thread communication is as follows:
174/// ```mermaid
175/// graph LR
176///     ambient[Ambient] -.-> heating
177///     tank_level_switch[Tank Level Switch] -.-> heating
178///     heating --> relay_manager[Relay Manager]
179///     relay_manager --> heating
180///     heating --> signal_handler[Signal Handler]
181///     signal_handler --> heating
182///     heating --> data_logger[Data Logger]
183///     data_logger --> heating
184///     heating --> schedule_check[Schedule Check]
185///     schedule_check --> heating
186///     messaging[Messaging] --> heating
187/// ```
188pub struct Heating {
189    /// configuration data for heating control
190    config: HeatingConfig,
191
192    /// inhibition flag to avoid flooding the log file with repeated messages about failure to receive information from schedule check
193    lock_error_channel_receive_schedule_check: bool,
194
195    /// inhibition flag to avoid flooding the log file with repeated messages about failure to send information to schedule check
196    lock_error_channel_send_schedule_check: bool,
197
198    /// recording when the last database ping happened
199    pub last_ping_instant: Instant,
200
201    /// database ping interval
202    pub database_ping_interval: Duration,
203
204    /// inhibition flag to avoid flooding the log file with repeated messages about excessive access time to mutex
205    pub lock_warn_max_mutex_access_duration: bool,
206
207    /// Flag to avoid flooding the log with warnings when the heating set values could not be read from the database
208    lock_error_heating_set_value_read_failure: bool,
209
210    // for testing purposes: record when mutex access time is exceeded without resetting it
211    #[cfg(test)]
212    pub mutex_access_duration_exceeded: bool,
213
214    /// Inhibition flag to avoid flooding the log with warning about having received inapplicable command from the signal handler
215    pub lock_warn_inapplicable_command_signal_handler: bool,
216
217    /// Inhibition flag to avoid flooding the log with errors about the channel communication not working
218    pub lock_error_channel_receive_termination: bool,
219
220    /// Maximum permissible access duration for Mutex
221    pub max_mutex_access_duration: Duration,
222}
223
224impl ProcessExternalRequestTrait for Heating {}
225
226impl Heating {
227    /// Creates a new `Heating` control instance.
228    ///
229    /// This constructor initializes the heating control module with its specific
230    /// configuration and a dedicated SQL interface. It also sets up all internal
231    /// "lock" flags to `false` by default; these flags are crucial during operation
232    /// to prevent log files from being flooded with repeated error or warning messages
233    /// stemming from channel communication or invalid requests.
234    ///
235    /// # Arguments
236    /// * `config` - **Configuration data** for the heating control, loaded from a TOML file.
237    ///   This includes parameters such as target temperatures, safety features, and
238    ///   behavioral strategies.
239    /// * `sql_interface_heating` - The **specific SQL interface** for heating-related database operations.
240    ///   This instance is moved into the `Heating` struct.
241    /// * `database_ping_interval` - A `Duration` instance, providing the interval to ping the database.
242    ///
243    /// # Returns
244    /// A new **`Heating` struct**, ready to manage the aquarium's heating system.
245    pub fn new(config: HeatingConfig, database_ping_interval: Duration) -> Heating {
246        Self {
247            config,
248            lock_error_channel_send_schedule_check: false,
249            lock_error_channel_receive_schedule_check: false,
250            last_ping_instant: Instant::now(),
251            database_ping_interval,
252            lock_warn_max_mutex_access_duration: false,
253            lock_error_heating_set_value_read_failure: false,
254            #[cfg(test)]
255            mutex_access_duration_exceeded: false,
256            lock_warn_inapplicable_command_signal_handler: false,
257            lock_error_channel_receive_termination: false,
258            max_mutex_access_duration: Duration::from_millis(MAX_MUTEX_ACCESS_DURATION_MILLIS),
259        }
260    }
261
262    /// Calculates a **normalized control deviation** based on the measured temperature and configured heating thresholds.
263    ///
264    /// This private helper function determines how far the `measured_value` deviates
265    /// from the `switch_on_temperature` relative to the total temperature control range
266    /// (`switch_on_temperature` to `switch_off_temperature`). The result is a float
267    /// between `0.0` and `1.0`. A value of `0.0` means the temperature is at or below
268    /// `switch_on_temperature` (full heating needed), and `1.0` means it's at or above
269    /// `switch_off_temperature` (no heating needed).
270    ///
271    /// # Arguments
272    /// * `measured_value` - The current measured temperature (`f32`).
273    ///
274    /// # Returns
275    /// An `f32` representing the normalized control deviation, ranging from `0.0` to `1.0`.
276    /// Returns `0.0` if `switch_off_temperature` is not greater than `switch_on_temperature`
277    /// (i.e., `target_value_delta` is not positive), indicating an invalid configuration.
278    fn calc_normalized_control_deviation(&self, measured_value: f32) -> f32 {
279        let target_value_delta: f32 =
280            self.config.switch_off_temperature - self.config.switch_on_temperature;
281        let measured_value_delta: f32 = measured_value - self.config.switch_on_temperature;
282        if target_value_delta > 0.0 {
283            measured_value_delta / target_value_delta
284        } else {
285            0.0 // default value when target values are not properly set up.
286        }
287    }
288
289    /// Retrieves the duration in seconds until the next midnight from the database.
290    ///
291    /// This private helper function acts as a robust wrapper around the SQL interface's
292    /// `get_duration_until_midnight` method.
293    ///
294    /// # Returns
295    /// An `i64` representing the number of seconds remaining until the next midnight.
296    ///
297    /// # Errors
298    /// This function handles errors internally. If the database call fails, it logs
299    /// the error and returns a default fallback value of 24 hours (86.400 seconds).
300    /// This prevents a crash and suspends statistics recording until the next day.
301    fn get_duration_until_midnight(
302        &mut self,
303        sql_interface_heating: &mut SqlInterfaceHeatingStats,
304    ) -> i64 {
305        sql_interface_heating
306            .get_duration_until_midnight()
307            .unwrap_or_else(|e| {
308                log_error_chain(module_path!(), "Could not get duration until midnight.", e);
309                24 * 3600 // default value for time in seconds until midnight
310            })
311    }
312
313    /// Executes the **main control loop for the heating module**.
314    ///
315    /// This function runs continuously, managing the aquarium's heating system until a termination
316    /// signal is received. It adjusts heater operation based on **water temperature** and
317    /// **tank level switch signals** adhering to the limitations imposed by a **schedule**.
318    /// The **ambient temperature** signal is considered for logging daily statistical data.
319    /// It also processes external `Start`/`Stop` commands, applies a
320    /// **sawtooth profile for proportional control**, and **logs daily heating statistics**.
321    ///
322    /// **Key Operations:**
323    /// - **Sensor Data Acquisition**: Reads current water temperature, ambient temperature, and tank level switch states from shared mutexes.
324    /// - **Safety Interlocks**: Implements a critical safety feature to turn off the heater if the water level is too low for a configured duration, preventing damage.
325    /// - **Schedule & External Control**: Checks with the `ScheduleCheck` module and responds to `Start`/`Stop` commands to inhibit or enable heating.
326    /// - **Proportional Control**: When within the active temperature range, uses a `SawToothProfile` to achieve a "PWM-like" heating effect.
327    /// - **Energy Logging**: Periodically updates and transfers daily heating statistics (energy consumption, average temperatures) to the SQL database.
328    /// - **Cycle Management**: Maintains a fixed cycle time, sleeping as necessary, and warning if execution duration exceeds the cycle limit.
329    /// - **Graceful Shutdown**: Responds to `Quit` and `Terminate` commands from the signal handler, ensuring a safe shutdown of the heater and final data logging.
330    ///
331    /// # Arguments
332    /// * `mutex_device_scheduler_heating` - A clone of the `Arc<Mutex<i32>>` used to coordinate device
333    ///   actuation across the application, preventing conflicts and tracking activity counts.
334    /// * `heating_channels` - A struct containing all sender and receiver channels for communication
335    ///   with modules like `RelayManager`, `SignalHandler`, `DataLogger`, `ScheduleCheck`, and `Messaging`.
336    /// * `heating_stats_transfer` - A mutable reference to an object implementing `HeatingStatsDataTransferTrait`,
337    ///   responsible for persisting daily heating statistics to the database.
338    /// * `heating_stats_transfer` - A mutable reference to an object implementing `HeatingSetValueUpdaterTrait`,
339    ///   responsible for updating the set values dynamically by communicating with SQL database.
340    /// * `heating_mutexes` - Struct containing the mutexes to access data from different threads.
341    ///
342    /// # Returns
343    /// This function does not return a value in the traditional sense, as it is designed
344    /// to loop indefinitely. It will only break out of its loop and terminate when a `Quit`
345    /// command is received from the signal handler, after which it performs final cleanup
346    /// (switching off the heater, logging final stats) and confirms shutdown.
347    pub fn execute(
348        &mut self,
349        mutex_device_scheduler_heating: Arc<Mutex<i32>>,
350        heating_channels: &mut HeatingChannels,
351        heating_stats_transfer: &mut impl HeatingStatsDataTransferTrait,
352        heating_set_val_updater: &mut impl ThermalSetValueUpdaterTrait,
353        heating_mutexes: HeatingMutexes,
354        mut sql_interface_heating: SqlInterfaceHeatingStats,
355    ) {
356        #[cfg(all(target_os = "linux", not(test)))]
357        info!(target: module_path!(), "Thread started with TID: {}", gettid());
358
359        let _refill_error = false;
360        let spin_sleeper = SpinSleeper::default();
361        let sleep_duration_hundred_millis = Duration::from_millis(100);
362        let mut saw_tooth_profile = SawToothProfile::new(&self.config.saw_tooth_profile_config);
363        let mut measured_water_temperature = 0.0;
364        let mut measurement_error: bool;
365        let mut measured_ambient_temperature = 0.0;
366        let mut measured_tank_level_switch_position = true;
367        let mut measured_tank_level_switch_invalid = false;
368        let mut loop_counter = 0;
369        let mut heater_state: ActuatorState = ActuatorState::Undefined;
370        let mut schedule_check_result: bool;
371        let mut duration_until_midnight =
372            self.get_duration_until_midnight(&mut sql_interface_heating);
373        let mut tank_level_low_counter = 0;
374        let mut energy_increment: f64 = 0.0;
375        let mut heating_inhibited: bool = false; // state of heating control determined by if the start/stop command has been received
376        let mut lock_warn_cycle_time_exceeded: bool = false;
377        let mut actuation_mutex_was_blocked_during_actuation: bool = false;
378        let cycle_time_duration = Duration::from_millis(CYCLE_TIME_HEATING_MILLIS);
379        let midnight_check_interval = Duration::from_secs(1);
380        let mut instant_midnight_check = Instant::now() - midnight_check_interval;
381
382        let mut heating_stats_opt = match sql_interface_heating
383            .get_single_heating_stats_from_database()
384        {
385            Ok(c) => Some(c),
386            Err(_) => {
387                #[cfg(test)]
388                println!(
389                    "{}: could not get existing heating stats entry for today from database - creating new one for today",
390                    module_path!()
391                );
392
393                Some(HeatingStatsEntry::new(&mut sql_interface_heating.conn))
394            }
395        };
396
397        let mut start_time = Instant::now();
398        loop {
399            let (
400                quit_command_received,  // the request to end the application has been received
401                start_command_received, // the request to (re-)start heating has been received
402                stop_command_received, // the request to (temporarily) stop heating has been received
403            ) = self.process_external_request(
404                &mut heating_channels.rx_heating_from_signal_handler,
405                heating_channels.rx_heating_from_messaging_opt.as_mut(),
406            );
407            if quit_command_received {
408                break;
409            }
410            if stop_command_received {
411                #[cfg(not(test))]
412                info!(
413                    target: module_path!(),
414                    "received Stop command. inhibiting heating."
415                );
416
417                heating_inhibited = true;
418            }
419            if start_command_received {
420                #[cfg(not(test))]
421                info!(
422                    target: module_path!(),
423                    "received Start command. restarting heating."
424                );
425
426                heating_inhibited = false;
427            }
428
429            if self.config.active {
430                // read signals from sensor manager
431                {
432                    match heating_mutexes.mutex_sensor_manager_signals.lock() {
433                        Ok(c) => {
434                            measured_water_temperature = c.water_temperature;
435                            measured_ambient_temperature = c.ambient_temperature;
436                            measurement_error = false;
437                        }
438                        Err(_) => {
439                            measurement_error = true;
440                        }
441                    };
442                }
443
444                // read tank level switch position and invalid bit from mutex
445                {
446                    (
447                        measured_tank_level_switch_position,
448                        measured_tank_level_switch_invalid,
449                    ) = match heating_mutexes.mutex_tank_level_switch_signals.lock() {
450                        Ok(c) => (c.tank_level_switch_position, c.tank_level_switch_invalid),
451                        Err(_) => {
452                            (
453                                measured_tank_level_switch_position,
454                                measured_tank_level_switch_invalid,
455                            ) // use previous values
456                        }
457                    }
458                }
459
460                // check if the tank-level switch hints to potentially low water level
461                if (measured_tank_level_switch_invalid) || (!measured_tank_level_switch_position) {
462                    tank_level_low_counter += 1;
463                } else {
464                    tank_level_low_counter = 0;
465                }
466
467                // schedule check to see if actuation is allowed
468                perform_schedule_check!(
469                    heating_channels,
470                    schedule_check_result,
471                    self.lock_error_channel_send_schedule_check,
472                    self.lock_error_channel_receive_schedule_check,
473                    module_path!()
474                );
475
476                // check if set values need to be updated
477                update_thermal_set_values!(
478                    heating_set_val_updater,
479                    self.config.switch_off_temperature,
480                    self.config.switch_on_temperature,
481                    self.lock_error_heating_set_value_read_failure,
482                    module_path!()
483                );
484
485                if self.config.stop_on_refill_error
486                    && (tank_level_low_counter > self.config.stop_on_refill_error_delay)
487                {
488                    // tank level switch position has not been available or indicating low water level for too long time
489                    // switch off the heater if not already off to protect against overheating of the device
490                    if heater_state != ActuatorState::Off {
491                        switch_off_heater(
492                            &mutex_device_scheduler_heating,
493                            &mut actuation_mutex_was_blocked_during_actuation,
494                            heating_channels,
495                        );
496                        heater_state = ActuatorState::Off;
497                    }
498                } else {
499                    // component protection allows operation of the heater
500                    if schedule_check_result && !heating_inhibited {
501                        // Schedule check said that actuation is allowed.
502                        // Also, there is no inhibition from external request.
503                        // The main control happens here (comparison measured/target, actuation).
504                        if (measured_water_temperature >= self.config.switch_on_temperature)
505                            && (measured_water_temperature <= self.config.switch_off_temperature)
506                        {
507                            // temperature is inside (dynamic) control window
508                            if (self.calc_normalized_control_deviation(measured_water_temperature)
509                                < saw_tooth_profile.level_normalized)
510                                && !measurement_error
511                            {
512                                // switch on the heater if not already on
513                                if heater_state != ActuatorState::On {
514                                    switch_on_heater(
515                                        &mutex_device_scheduler_heating,
516                                        &mut actuation_mutex_was_blocked_during_actuation,
517                                        heating_channels,
518                                    );
519                                    heater_state = ActuatorState::On;
520                                }
521                            } else {
522                                // switch off the heater if not already off
523                                if heater_state != ActuatorState::Off {
524                                    switch_off_heater(
525                                        &mutex_device_scheduler_heating,
526                                        &mut actuation_mutex_was_blocked_during_actuation,
527                                        heating_channels,
528                                    );
529                                    heater_state = ActuatorState::Off;
530                                }
531                            }
532                        } else {
533                            // the temperature is either above or below (dynamic) control window
534                            if measured_water_temperature > self.config.switch_off_temperature {
535                                // switch off the heater if not already off
536                                if heater_state != ActuatorState::Off {
537                                    switch_off_heater(
538                                        &mutex_device_scheduler_heating,
539                                        &mut actuation_mutex_was_blocked_during_actuation,
540                                        heating_channels,
541                                    );
542                                    heater_state = ActuatorState::Off;
543                                }
544                            }
545                            if measured_water_temperature < self.config.switch_on_temperature {
546                                // switch on the heater if not already on
547                                if heater_state != ActuatorState::On {
548                                    switch_on_heater(
549                                        &mutex_device_scheduler_heating,
550                                        &mut actuation_mutex_was_blocked_during_actuation,
551                                        heating_channels,
552                                    );
553                                    heater_state = ActuatorState::On;
554                                }
555                            }
556                        }
557                    } else if heating_inhibited {
558                        // the external request to stop heating control has been received
559                        match self.config.switch_on_when_external_stop {
560                            true => {
561                                // switch on the heater if not already on
562                                if heater_state != ActuatorState::On {
563                                    switch_on_heater(
564                                        &mutex_device_scheduler_heating,
565                                        &mut actuation_mutex_was_blocked_during_actuation,
566                                        heating_channels,
567                                    );
568                                    heater_state = ActuatorState::On;
569                                }
570                            }
571                            false => {
572                                // switch off the heater if not already off
573                                if heater_state != ActuatorState::Off {
574                                    switch_off_heater(
575                                        &mutex_device_scheduler_heating,
576                                        &mut actuation_mutex_was_blocked_during_actuation,
577                                        heating_channels,
578                                    );
579                                    heater_state = ActuatorState::Off;
580                                }
581                            }
582                        }
583                    } else {
584                        // schedule check said that we are not allowed to actuate
585                        match self.config.switch_on_when_out_of_schedule {
586                            true => {
587                                // switch on the heater if not already on
588                                if heater_state != ActuatorState::On {
589                                    switch_on_heater(
590                                        &mutex_device_scheduler_heating,
591                                        &mut actuation_mutex_was_blocked_during_actuation,
592                                        heating_channels,
593                                    );
594                                    heater_state = ActuatorState::On;
595                                }
596                            }
597                            false => {
598                                // switch off the heater if not already off
599                                if heater_state != ActuatorState::Off {
600                                    switch_off_heater(
601                                        &mutex_device_scheduler_heating,
602                                        &mut actuation_mutex_was_blocked_during_actuation,
603                                        heating_channels,
604                                    );
605                                    heater_state = ActuatorState::Off;
606                                }
607                            }
608                        }
609                    }
610                }
611
612                saw_tooth_profile.execute_with(TIME_INCREMENT_HEATING_SECS);
613
614                // the update of midnight counter only runs every second
615                if instant_midnight_check.elapsed() >= midnight_check_interval {
616                    // one second closer to midnight
617                    instant_midnight_check = Instant::now();
618                    duration_until_midnight = duration_until_midnight.saturating_sub(1);
619                    match heating_stats_opt {
620                        Some(ref mut heating_stats) => {
621                            // wrap temperatures depending on the status bits
622                            let water_temperature_opt = match measurement_error {
623                                true => None,
624                                false => Some(measured_water_temperature as f64),
625                            };
626                            let ambient_temperature_opt = match measurement_error {
627                                true => None,
628                                false => Some(measured_ambient_temperature as f64),
629                            };
630                            // update the heating statistics with recently acquired data
631                            if heater_state == ActuatorState::On {
632                                energy_increment = (self.config.heater_power / 3600.0) as f64;
633                            }
634                            heating_stats.update(
635                                water_temperature_opt,
636                                ambient_temperature_opt,
637                                energy_increment,
638                            );
639                        }
640                        None => {
641                            // do nothing, heating stats or not mandatory to run heating control
642                        }
643                    }
644
645                    // check if midnight has arrived
646                    if duration_until_midnight <= 0 {
647                        // Use take() to move the value out of the Option without cloning.
648                        if let Some(stats) = heating_stats_opt.take() {
649                            if let Err(e) = heating_stats_transfer
650                                .transfer_heating_stats(stats, &mut sql_interface_heating)
651                            {
652                                error!("Error occurred when trying to store heating stats data in database: {e:?}");
653                            }
654                            // Reset heating stats
655                            heating_stats_opt =
656                                Some(HeatingStatsEntry::new(&mut sql_interface_heating.conn));
657                            duration_until_midnight =
658                                self.get_duration_until_midnight(&mut sql_interface_heating);
659                        }
660                    }
661                }
662            }
663
664            let instant_before_locking_mutex = Instant::now();
665            let mut instant_after_locking_mutex = Instant::now(); // initialization is overwritten
666
667            // Write signal to mutex
668            {
669                match heating_mutexes.mutex_heating_status.lock() {
670                    Ok(mut c) => {
671                        instant_after_locking_mutex = Instant::now();
672                        *c = match heater_state {
673                            ActuatorState::On => true,
674                            ActuatorState::Off => false,
675                            _ => false,
676                        }
677                    }
678                    Err(_) => {
679                        // Do nothing
680                    }
681                }
682            }
683
684            // check if access to mutex took too long
685            self.check_mutex_access_duration(
686                None,
687                instant_after_locking_mutex,
688                instant_before_locking_mutex,
689            );
690
691            loop_counter += 1;
692
693            // avoid overflow
694            if loop_counter == 1_000_000 {
695                loop_counter = 0;
696            }
697
698            // Manage the loop's cycle time, sleeping or logging a warning as needed.
699            manage_cycle_time_thermal!(
700                start_time,
701                cycle_time_duration,
702                CYCLE_TIME_HEATING_MILLIS,
703                spin_sleeper,
704                lock_warn_cycle_time_exceeded,
705                actuation_mutex_was_blocked_during_actuation,
706                module_path!()
707            );
708
709            self.check_timing_and_ping_database(&mut sql_interface_heating);
710        }
711
712        // switch off the heater if not already off
713        if heater_state != ActuatorState::Off {
714            switch_off_heater(
715                &mutex_device_scheduler_heating,
716                &mut actuation_mutex_was_blocked_during_actuation,
717                heating_channels,
718            );
719        }
720
721        // update the database with the heating stats data recorded
722        // so far for this date
723        if let Some(stats) = heating_stats_opt.take() {
724            if let Err(e) =
725                heating_stats_transfer.transfer_heating_stats(stats, &mut sql_interface_heating)
726            {
727                error!("Error occurred when trying to store heating stats data in database (during termination): {e:?}");
728            }
729        }
730
731        heating_channels.acknowledge_signal_handler();
732
733        // This thread has channel connections to underlying threads.
734        // Those threads have to stop receiving commands from this thread.
735        // The shutdown sequence is handled by the signal_handler module.
736        self.wait_for_termination(
737            &mut heating_channels.rx_heating_from_signal_handler,
738            sleep_duration_hundred_millis,
739            module_path!(),
740        );
741    }
742}
743
744#[cfg(test)]
745pub mod tests {
746    use all_asserts::{assert_ge, assert_le};
747    use chrono::Local;
748    use mysql::PooledConn;
749    use spin_sleep::SpinSleeper;
750    use std::sync::{Arc, Mutex};
751    use std::thread;
752    use std::time::{Duration, Instant};
753
754    use crate::database::sql_interface_heating_stats::{
755        HeatingStatsEntry, SqlInterfaceHeatingStats,
756    };
757    use crate::database::sql_interface_midnight::SqlInterfaceMidnightCalculatorTrait;
758    use crate::database::{sql_interface::SqlInterface, sql_interface_error::SqlInterfaceError};
759    use crate::launch::channels::Channels;
760    use crate::mocks::mock_heating::tests::MockSqlInterfaceHeatingSetVals;
761    use crate::mocks::mock_relay_manager::tests::mock_relay_manager;
762    use crate::mocks::mock_schedule_check::tests::mock_schedule_check;
763    use crate::sensors::sensor_manager::SensorManagerSignals;
764    use crate::sensors::tank_level_switch::TankLevelSwitchSignals;
765    use crate::thermal::heating::{Heating, HeatingMutexes, HeatingStatsDataTransferTrait};
766    use crate::utilities::channel_content::{AquariumDevice, InternalCommand};
767    use crate::utilities::common::tests::update_min_max_actuation_duration;
768    use crate::utilities::config::{read_config_file_with_test_database, ConfigData};
769    use crate::utilities::sawtooth_profile::SawToothProfileConfig;
770
771    pub struct HeatingStatsMockDataTransfer {
772        heating_stats_entry_previous: HeatingStatsEntry,
773        heating_stats_entry: HeatingStatsEntry,
774    }
775
776    impl HeatingStatsMockDataTransfer {
777        // Creates a new `HeatingStatsMockDataTransfer` instance.
778        //
779        // This constructor initializes the mock data transfer object for testing heating statistics.
780        // It sets up two `HeatingStatsEntry` instances, both initialized with default (zero) values
781        // for the current date, to simulate the previous and current day's statistics.
782        //
783        // # Returns
784        // A new `HeatingStatsMockDataTransfer` struct.
785        pub fn new(conn: &mut PooledConn) -> HeatingStatsMockDataTransfer {
786            Self {
787                heating_stats_entry_previous: HeatingStatsEntry::new(conn),
788                heating_stats_entry: HeatingStatsEntry::new(conn),
789            }
790        }
791    }
792
793    impl HeatingStatsDataTransferTrait for HeatingStatsMockDataTransfer {
794        // Transfers provides daily heating statistics entry to the SQL database.
795        //
796        // This implementation updates the `date` of the `heating_stats_entry` to the
797        // current database date (ensuring consistency) and then attempts to insert
798        // or update this record in the database. Errors during database operations are logged.
799        //
800        // # Arguments
801        // * `heating_stats_entry` - The `HeatingStatsEntry` containing the statistics for the day.
802        //   This is passed by value, meaning ownership is transferred to this function.
803        // * `sql_interface_heating` - A mutable reference to the `SqlInterfaceHeating` instance,
804        //   used for database interaction.
805        fn transfer_heating_stats(
806            &mut self,
807            heating_stats_entry: HeatingStatsEntry,
808            _sql_interface_heating: &mut SqlInterfaceHeatingStats,
809        ) -> Result<(), SqlInterfaceError> {
810            self.heating_stats_entry_previous = self.heating_stats_entry.clone();
811            self.heating_stats_entry = heating_stats_entry.clone();
812            println!(
813                "Heating: Received command to transfer mock data to SQL database: {}",
814                heating_stats_entry,
815            );
816            Ok(())
817        }
818    }
819
820    pub struct SqlInterfaceHeatingMockMidnightCalculator {
821        seconds_to_midnight: i64,
822    }
823
824    impl SqlInterfaceHeatingMockMidnightCalculator {
825        // Creates a new `SqlInterfaceHeatingMockMidnightCalculator` instance.
826        //
827        // This constructor initializes the mock calculator, setting the specific
828        // number of seconds until midnight that it will consistently return during tests.
829        // This allows for predictable simulation of time progression in testing scenarios.
830        //
831        // # Arguments
832        // * `seconds_to_midnight` - The fixed `i64` value representing the number of
833        //   seconds until midnight that this mock will provide.
834        //
835        // # Returns
836        // A new `SqlInterfaceHeatingMockMidnightCalculator` struct.
837        pub fn new(seconds_to_midnight: i64) -> SqlInterfaceHeatingMockMidnightCalculator {
838            Self {
839                seconds_to_midnight,
840            }
841        }
842    }
843
844    impl SqlInterfaceMidnightCalculatorTrait for SqlInterfaceHeatingMockMidnightCalculator {
845        // Provides a predefined duration until midnight for testing heating statistics transfer.
846        //
847        // This mock implementation of `get_duration_until_midnight` always returns a
848        // fixed `i64` value, `self.seconds_to_midnight`. This allows tests to simulate
849        // various durations until midnight without actually waiting for real-world time to pass,
850        // enabling deterministic and fast testing of the heating statistics transfer logic.
851        //
852        // # Returns
853        // An `Ok(i64)` containing the predefined number of seconds until midnight.
854        // This function will always succeed (`Ok`) as it does not interact with a real database.
855        fn get_duration_until_midnight(&mut self) -> Result<i64, SqlInterfaceError> {
856            Ok(self.seconds_to_midnight) // seconds to midnight after which heating stats are inserted into the database
857                                         // for testing, we do not want to wait until midnight
858        }
859    }
860
861    // Prepares a `Heating` control instance with a mock midnight calculator and a clean test database.
862    //
863    // This helper function is designed exclusively for testing the `Heating` module. It sets up
864    // a controlled environment by:
865    // 1.  Loading a generic test configuration and overriding specific settings (e.g., `stop_on_refill_error_delay`).
866    // 2.  Optionally, applying a custom `SawToothProfileConfig` for precise control over heating patterns.
867    // 3.  Connecting to and **truncating a dedicated test database** to ensure a clean state for heating statistics.
868    // 4.  Injecting a **`SqlInterfaceHeatingMockMidnightCalculator`** to return a predefined `seconds_to_midnight`,
869    //     allowing deterministic testing of daily statistic transfers without real-time dependency.
870    //
871    // # Arguments
872    // * `seconds_to_midnight` - The fixed number of seconds (as `i64`) the mock midnight calculator will return.
873    //   This directly controls when the `Heating` module attempts to transfer daily statistics.
874    // * `test_db_number` - A unique `u32` identifier (e.g., `7`, `8`, `9`) used to select a specific
875    //   test database (`aquarium_test_XX`) from the configuration.
876    // * `saw_tooth_profile_config_opt` - An `Option<SawToothProfileConfig>`. If `Some`, this custom
877    //   sawtooth profile configuration overrides the one loaded from the TOML file for the `Heating` module.
878    //
879    // # Returns
880    // A tuple `(ConfigData, Heating)`:
881    // - The `ConfigData` struct, containing the loaded and modified configuration.
882    // - The fully initialized `Heating` struct, ready for test execution.
883    //
884    // # Panics
885    // This function will panic if:
886    // - `read_config_file_with_test_database` fails (e.g., file not found, invalid DB number).
887    // - It fails to connect to the SQL database.
888    // - It fails to truncate the `heatingstats` table in the database.
889    // - It encounters any issue during the initialization of the `SqlInterfaceHeating` or `Heating` structs.
890    pub fn prepare_heating_tests_mock_midnight_calculator(
891        seconds_to_midnight: i64,
892        test_db_number: u32,
893        saw_tooth_profile_config_opt: Option<SawToothProfileConfig>,
894    ) -> (ConfigData, Heating, SqlInterface, SqlInterfaceHeatingStats) {
895        // prepare struct Heating for further tests
896        let mut config: ConfigData = read_config_file_with_test_database(
897            "/config/aquarium_control_test_generic.toml".to_string(),
898            test_db_number,
899        );
900        config.heating.stop_on_refill_error_delay = 2;
901
902        match saw_tooth_profile_config_opt {
903            Some(saw_tooth_profile_config) => {
904                config.heating.saw_tooth_profile_config = saw_tooth_profile_config;
905            }
906            None => { /* do nothing */ }
907        }
908
909        println!("Testing with database {}", config.sql_interface.db_name);
910        let mut sql_interface = match SqlInterface::new(config.sql_interface.clone()) {
911            Ok(c) => c,
912            Err(e) => {
913                panic!("Could not connect to SQL database: {e:?}");
914            }
915        };
916
917        // empty the heating stats table
918        match SqlInterface::truncate_table(&mut sql_interface, "heatingstats".to_string()) {
919            Ok(_) => {}
920            Err(e) => {
921                panic!("Could not prepare test case: {e:?}")
922            }
923        }
924
925        let sql_interface_heating_midnight_calculator = Box::new(
926            SqlInterfaceHeatingMockMidnightCalculator::new(seconds_to_midnight),
927        );
928        let sql_interface_heating_stats = SqlInterfaceHeatingStats::new(
929            sql_interface.get_connection().unwrap(),
930            config.sql_interface.max_rows_heating_stats,
931            sql_interface_heating_midnight_calculator,
932        )
933        .unwrap();
934
935        println!(
936            "prepare_heating_tests_mock_midnight_calculator: SwitchOff temperature = {}, SwitchOn temperature = {}",
937            config.heating.switch_on_temperature, config.heating.switch_off_temperature
938        );
939        let heating_config = config.heating.clone();
940        (
941            config,
942            Heating::new(heating_config, Duration::from_secs(1000)),
943            sql_interface,
944            sql_interface_heating_stats,
945        )
946    }
947
948    #[test]
949    // Check if the heater remains switched off with a high temperature.
950    // Also, implicitly test if the execute function terminates after receiving Quit and Terminate commands.
951    // Test case uses test database #11.
952    pub fn test_heating_with_measured_temperature_high() {
953        let sleep_duration_100_millis = Duration::from_millis(100);
954        let spin_sleeper = SpinSleeper::default();
955
956        let (mut config, mut heating, sql_interface, sql_interface_heating_stats) =
957            prepare_heating_tests_mock_midnight_calculator(3600, 11, None);
958
959        // replacement values are used to initialize the mutexes
960        config.sensor_manager.replacement_value_water_temperature = 30.0;
961        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
962
963        let mut channels = Channels::new_for_test();
964
965        // Mutex for tank level switch signals
966        let mutex_tank_level_switch_signals =
967            Arc::new(Mutex::new(TankLevelSwitchSignals::new(true, true, false)));
968
969        // thread for mock schedule check
970        let join_handle_mock_schedule_check = thread::Builder::new()
971            .name("mock_schedule_check".to_string())
972            .spawn(move || {
973                mock_schedule_check(
974                    &mut channels.schedule_check.tx_schedule_check_to_heating,
975                    &mut channels.schedule_check.rx_schedule_check_from_heating,
976                    None,
977                    true,
978                );
979            })
980            .unwrap();
981
982        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
983
984        // thread for mock relay manager - includes assertions
985        let join_handle_mock_relay_manager = thread::Builder::new()
986            .name("mock_relay_manager".to_string())
987            .spawn(move || {
988                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
989                    &mut channels.relay_manager.tx_relay_manager_to_heating,
990                    &mut channels.relay_manager.rx_relay_manager_from_heating,
991                );
992                assert_eq!(actuation_events.len(), 1);
993                let actuation_event = actuation_events.pop().unwrap();
994                assert_eq!(
995                    actuation_event.command,
996                    InternalCommand::SwitchOff(AquariumDevice::Heater)
997                );
998                mock_actuator_states.check_terminal_condition_heating();
999            })
1000            .unwrap();
1001
1002        // thread for controlling duration of test run
1003        let join_handle_test_environment = thread::Builder::new()
1004            .name("test_environment".to_string())
1005            .spawn(move || {
1006                let sleep_duration_ten_seconds = Duration::from_secs(10);
1007                let spin_sleeper = SpinSleeper::default();
1008                spin_sleeper.sleep(sleep_duration_ten_seconds);
1009                let _ = channels
1010                    .signal_handler
1011                    .send_to_heating(InternalCommand::Quit);
1012                channels.signal_handler.receive_from_heating().unwrap();
1013                let _ = channels
1014                    .signal_handler
1015                    .send_to_heating(InternalCommand::Terminate);
1016            })
1017            .unwrap();
1018
1019        // make sure all mock threads are running when instantiating the test object
1020        spin_sleeper.sleep(sleep_duration_100_millis);
1021
1022        // thread for the test object
1023        let join_handle_test_object = thread::Builder::new()
1024            .name("test_object".to_string())
1025            .spawn(move || {
1026                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1027                let mut tx_heating_to_schedule_check_for_test_case_finish =
1028                    channels.heating.tx_heating_to_schedule_check.clone();
1029                let mut tx_heating_to_relay_manager_for_test_case_finish =
1030                    channels.heating.tx_heating_to_relay_manager.clone();
1031
1032                let mut heating_stats_transfer =
1033                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
1034                let mut heating_set_value_updater =
1035                    MockSqlInterfaceHeatingSetVals::new(false, None, None, None);
1036
1037                let heating_mutexes = HeatingMutexes {
1038                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(
1039                        &config.sensor_manager,
1040                    ))),
1041                    mutex_tank_level_switch_signals,
1042                    mutex_heating_status: Arc::new(Mutex::new(false)),
1043                };
1044
1045                heating.execute(
1046                    mutex_device_scheduler_heating.clone(),
1047                    &mut channels.heating,
1048                    &mut heating_stats_transfer,
1049                    &mut heating_set_value_updater,
1050                    heating_mutexes,
1051                    sql_interface_heating_stats,
1052                );
1053
1054                // send Quit signal to mock threads because the test object has terminated
1055                let _ =
1056                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
1057                let _ =
1058                    tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
1059
1060                println!("* [Heating] checking reaction to high temperature succeeded.");
1061            })
1062            .unwrap();
1063
1064        join_handle_mock_schedule_check
1065            .join()
1066            .expect("Mock schedule check did not finish.");
1067        join_handle_mock_relay_manager
1068            .join()
1069            .expect("Mock relay manager thread did not finish.");
1070        join_handle_test_environment
1071            .join()
1072            .expect("Test environment thread did not finish.");
1073        join_handle_test_object
1074            .join()
1075            .expect("Test object thread did not finish.");
1076    }
1077
1078    #[test]
1079    // Check if the heater is switched on and is not switched off with low temperature.
1080    // Also, implicitly test if the execute function terminates after receiving Quit and Terminate commands.
1081    // Check if the heater is switched off after sending Quit command and before sending Terminate command.
1082    // Test case uses test database #12.
1083    pub fn test_heating_with_measured_temperature_low_without_mutex_blocked() {
1084        let sleep_duration_100_millis = Duration::from_millis(100);
1085        let spin_sleeper = SpinSleeper::default();
1086
1087        let (mut config, mut heating, sql_interface, sql_interface_heating_stats) =
1088            prepare_heating_tests_mock_midnight_calculator(3600, 12, None);
1089
1090        // replacement values are used to initialize the mutexes
1091        config.sensor_manager.replacement_value_water_temperature = 20.0;
1092        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
1093
1094        let mut channels = Channels::new_for_test();
1095
1096        // Mutex for tank level switch signals
1097        let mutex_tank_level_switch_signals =
1098            Arc::new(Mutex::new(TankLevelSwitchSignals::new(true, true, false)));
1099
1100        // thread for mock schedule check
1101        let join_handle_mock_schedule_check = thread::Builder::new()
1102            .name("mock_schedule_check".to_string())
1103            .spawn(move || {
1104                mock_schedule_check(
1105                    &mut channels.schedule_check.tx_schedule_check_to_heating,
1106                    &mut channels.schedule_check.rx_schedule_check_from_heating,
1107                    None,
1108                    true,
1109                );
1110            })
1111            .unwrap();
1112
1113        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
1114
1115        // thread for mock relay manager - includes assertions
1116        let join_handle_mock_relay_manager = thread::Builder::new()
1117            .name("mock_relay_manager".to_string())
1118            .spawn(move || {
1119                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
1120                    &mut channels.relay_manager.tx_relay_manager_to_heating,
1121                    &mut channels.relay_manager.rx_relay_manager_from_heating,
1122                );
1123                assert_eq!(actuation_events.len(), 2);
1124                let last_actuation_event = actuation_events.pop().unwrap();
1125                assert_eq!(
1126                    last_actuation_event.command,
1127                    InternalCommand::SwitchOff(AquariumDevice::Heater)
1128                );
1129                let first_actuation_event = actuation_events.pop().unwrap();
1130                assert_eq!(
1131                    first_actuation_event.command,
1132                    InternalCommand::SwitchOn(AquariumDevice::Heater)
1133                );
1134                mock_actuator_states.check_terminal_condition_heating();
1135            })
1136            .unwrap();
1137
1138        // thread for controlling duration of test run
1139        let join_handle_test_environment = thread::Builder::new()
1140            .name("test_environment".to_string())
1141            .spawn(move || {
1142                let sleep_duration_ten_seconds = Duration::from_secs(10);
1143                let spin_sleeper = SpinSleeper::default();
1144                spin_sleeper.sleep(sleep_duration_ten_seconds);
1145                let _ = channels
1146                    .signal_handler
1147                    .send_to_heating(InternalCommand::Quit);
1148                channels.signal_handler.receive_from_heating().unwrap();
1149                let _ = channels
1150                    .signal_handler
1151                    .send_to_heating(InternalCommand::Terminate);
1152            })
1153            .unwrap();
1154
1155        // make sure all mock threads are running when instantiating the test object
1156        spin_sleeper.sleep(sleep_duration_100_millis);
1157
1158        // thread for the test object
1159        let join_handle_test_object = thread::Builder::new()
1160            .name("test_object".to_string())
1161            .spawn(move || {
1162                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1163                let mut tx_heating_to_schedule_check_for_test_case_finish =
1164                    channels.heating.tx_heating_to_schedule_check.clone();
1165                let mut tx_heating_to_relay_manager_for_test_case_finish =
1166                    channels.heating.tx_heating_to_relay_manager.clone();
1167
1168                let mut heating_stats_transfer =
1169                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
1170                let mut heating_set_value_updater =
1171                    MockSqlInterfaceHeatingSetVals::new(false, None, None, None);
1172
1173                let heating_mutexes = HeatingMutexes {
1174                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(
1175                        &config.sensor_manager,
1176                    ))),
1177                    mutex_tank_level_switch_signals,
1178                    mutex_heating_status: Arc::new(Mutex::new(false)),
1179                };
1180
1181                heating.execute(
1182                    mutex_device_scheduler_heating.clone(),
1183                    &mut channels.heating,
1184                    &mut heating_stats_transfer,
1185                    &mut heating_set_value_updater,
1186                    heating_mutexes,
1187                    sql_interface_heating_stats,
1188                );
1189
1190                // send Quit signal to mock threads because the test object has terminated
1191                let _ =
1192                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
1193                let _ =
1194                    tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
1195
1196                println!("* [Heating] checking reaction to low temperature succeeded.");
1197            })
1198            .unwrap();
1199
1200        join_handle_mock_schedule_check
1201            .join()
1202            .expect("Mock schedule check did not finish.");
1203        join_handle_mock_relay_manager
1204            .join()
1205            .expect("Mock relay manager thread did not finish.");
1206        join_handle_test_environment
1207            .join()
1208            .expect("Test environment thread did not finish.");
1209        join_handle_test_object
1210            .join()
1211            .expect("Test object thread did not finish.");
1212    }
1213
1214    #[test]
1215    // Check if the heater is switched on and off during two cycles of the saw tooth profile.
1216    // The duration of the heater being switched on shall equal the duration of the heater switched off (1% tolerance).
1217    // Also, implicitly test if the execute function terminates after receiving Quit and Terminate commands.
1218    // Test case uses test database #13.
1219    pub fn test_heating_with_measured_temperature_50_percent() {
1220        let sleep_duration_100_millis = Duration::from_millis(100);
1221        let spin_sleeper = SpinSleeper::default();
1222
1223        let (mut config, mut heating, sql_interface, sql_interface_heating_stats) =
1224            prepare_heating_tests_mock_midnight_calculator(
1225                3600,
1226                13,
1227                Some(SawToothProfileConfig::new(6, 1, 6, 1)),
1228            );
1229
1230        // replacement values are used to initialize the mutexes
1231        config.sensor_manager.replacement_value_water_temperature = 25.0;
1232        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
1233
1234        let mut channels = Channels::new_for_test();
1235
1236        // Mutex for tank level switch signals
1237        let mutex_tank_level_switch_signals =
1238            Arc::new(Mutex::new(TankLevelSwitchSignals::new(true, true, false)));
1239
1240        // thread for mock schedule check
1241        let join_handle_mock_schedule_check = thread::Builder::new()
1242            .name("mock_schedule_check".to_string())
1243            .spawn(move || {
1244                mock_schedule_check(
1245                    &mut channels.schedule_check.tx_schedule_check_to_heating,
1246                    &mut channels.schedule_check.rx_schedule_check_from_heating,
1247                    None,
1248                    true,
1249                );
1250            })
1251            .unwrap();
1252
1253        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
1254
1255        // thread for mock relay manager - includes assertions
1256        let join_handle_mock_relay_manager = thread::Builder::new()
1257            .name("mock_relay_manager".to_string())
1258            .spawn(move || {
1259                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
1260                    &mut channels.relay_manager.tx_relay_manager_to_heating,
1261                    &mut channels.relay_manager.rx_relay_manager_from_heating,
1262                );
1263                println!(
1264                    "Mock relay manager received {} commands:",
1265                    actuation_events.len()
1266                );
1267                for actuation_event in actuation_events.clone() {
1268                    println!("{}", actuation_event);
1269                }
1270                // (first and) last actuation events need to be ignored
1271                let _last_actuation_event = actuation_events.pop().unwrap();
1272                let second_last_actuation_event = actuation_events.pop().unwrap();
1273                let third_last_actuation_event = actuation_events.pop().unwrap();
1274                let fourth_last_actuation_event = actuation_events.pop().unwrap();
1275                let fifth_last_actuation_event = actuation_events.pop().unwrap();
1276                let sixth_last_actuation_event = actuation_events.pop().unwrap();
1277                let seventh_last_actuation_event = actuation_events.pop().unwrap();
1278
1279                let mut min_actuation_duration: u128 = 100000;
1280                let mut max_actuation_duration: u128 = 0;
1281
1282                let second_last_actuation_duration = second_last_actuation_event
1283                    .time
1284                    .duration_since(third_last_actuation_event.time)
1285                    .as_millis();
1286                (min_actuation_duration, max_actuation_duration) =
1287                    update_min_max_actuation_duration(
1288                        min_actuation_duration,
1289                        max_actuation_duration,
1290                        second_last_actuation_duration,
1291                    );
1292                println!(
1293                    "second_last_actuation_duration={}",
1294                    second_last_actuation_duration
1295                );
1296
1297                let third_last_actuation_duration = third_last_actuation_event
1298                    .time
1299                    .duration_since(fourth_last_actuation_event.time)
1300                    .as_millis();
1301                (min_actuation_duration, max_actuation_duration) =
1302                    update_min_max_actuation_duration(
1303                        min_actuation_duration,
1304                        max_actuation_duration,
1305                        second_last_actuation_duration,
1306                    );
1307                println!(
1308                    "third_last_actuation_duration={}",
1309                    third_last_actuation_duration
1310                );
1311
1312                let fourth_last_actuation_duration = fourth_last_actuation_event
1313                    .time
1314                    .duration_since(fifth_last_actuation_event.time)
1315                    .as_millis();
1316                (min_actuation_duration, max_actuation_duration) =
1317                    update_min_max_actuation_duration(
1318                        min_actuation_duration,
1319                        max_actuation_duration,
1320                        second_last_actuation_duration,
1321                    );
1322                println!(
1323                    "fourth_last_actuation_duration={}",
1324                    fourth_last_actuation_duration
1325                );
1326
1327                let fifth_last_actuation_duration = fifth_last_actuation_event
1328                    .time
1329                    .duration_since(sixth_last_actuation_event.time)
1330                    .as_millis();
1331                (min_actuation_duration, max_actuation_duration) =
1332                    update_min_max_actuation_duration(
1333                        min_actuation_duration,
1334                        max_actuation_duration,
1335                        second_last_actuation_duration,
1336                    );
1337                println!(
1338                    "fifth_last_actuation_duration={}",
1339                    fifth_last_actuation_duration
1340                );
1341
1342                let sixth_last_actuation_duration = sixth_last_actuation_event
1343                    .time
1344                    .duration_since(seventh_last_actuation_event.time)
1345                    .as_millis();
1346                (min_actuation_duration, max_actuation_duration) =
1347                    update_min_max_actuation_duration(
1348                        min_actuation_duration,
1349                        max_actuation_duration,
1350                        second_last_actuation_duration,
1351                    );
1352                println!(
1353                    "sixth_last_actuation_duration={}",
1354                    sixth_last_actuation_duration
1355                );
1356
1357                let delta_min_max_duration = max_actuation_duration - min_actuation_duration;
1358
1359                // execution timing differs between platforms.
1360                // hence, platform-specific asserts are necessary
1361                cfg_if::cfg_if! {
1362                    if #[cfg(target_os = "linux")] {
1363                        assert_le!(second_last_actuation_duration, 7100);
1364                        assert_ge!(second_last_actuation_duration, 6400);
1365                        assert_le!(third_last_actuation_duration, 7100);
1366                        assert_ge!(third_last_actuation_duration, 6400);
1367                        assert_le!(fourth_last_actuation_duration, 7100);
1368                        assert_ge!(fourth_last_actuation_duration, 6400);
1369                        assert_le!(fifth_last_actuation_duration, 7100);
1370                        assert_ge!(fifth_last_actuation_duration, 6400);
1371                        assert_le!(sixth_last_actuation_duration, 7100);
1372                        assert_ge!(sixth_last_actuation_duration, 6400);
1373                }
1374                    else {
1375                        assert_le!(second_last_actuation_duration, 7400);
1376                        assert_ge!(second_last_actuation_duration, 6400);
1377                        assert_le!(third_last_actuation_duration, 7400);
1378                        assert_ge!(third_last_actuation_duration, 6400);
1379                        assert_le!(fourth_last_actuation_duration, 7400);
1380                        assert_ge!(fourth_last_actuation_duration, 6400);
1381                        assert_le!(fifth_last_actuation_duration, 7400);
1382                        assert_ge!(fifth_last_actuation_duration, 6400);
1383                        assert_le!(sixth_last_actuation_duration, 7400);
1384                        assert_ge!(sixth_last_actuation_duration, 6400);
1385                    }
1386                }
1387                assert_le!(delta_min_max_duration, 50);
1388                mock_actuator_states.check_terminal_condition_heating();
1389            })
1390            .unwrap();
1391
1392        // thread for controlling duration of test run
1393        let join_handle_test_environment = thread::Builder::new()
1394            .name("test_environment".to_string())
1395            .spawn(move || {
1396                let sleep_duration = Duration::from_secs(59);
1397                let spin_sleeper = SpinSleeper::default();
1398                spin_sleeper.sleep(sleep_duration);
1399                let _ = channels
1400                    .signal_handler
1401                    .send_to_heating(InternalCommand::Quit);
1402                channels.signal_handler.receive_from_heating().unwrap();
1403                let _ = channels
1404                    .signal_handler
1405                    .send_to_heating(InternalCommand::Terminate);
1406            })
1407            .unwrap();
1408
1409        // make sure all mock threads are running when instantiating the test object
1410        spin_sleeper.sleep(sleep_duration_100_millis);
1411
1412        // thread for the test object
1413        let join_handle_test_object = thread::Builder::new()
1414            .name("test_object".to_string())
1415            .spawn(move || {
1416                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1417                let mut tx_heating_to_schedule_check_for_test_case_finish =
1418                    channels.heating.tx_heating_to_schedule_check.clone();
1419                let mut tx_heating_to_relay_manager_for_test_case_finish =
1420                    channels.heating.tx_heating_to_relay_manager.clone();
1421
1422                let mut heating_stats_transfer =
1423                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
1424                let mut heating_set_value_updater =
1425                    MockSqlInterfaceHeatingSetVals::new(false, None, None, None);
1426
1427                let heating_mutexes = HeatingMutexes {
1428                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(
1429                        &config.sensor_manager,
1430                    ))),
1431                    mutex_tank_level_switch_signals,
1432                    mutex_heating_status: Arc::new(Mutex::new(false)),
1433                };
1434
1435                heating.execute(
1436                    mutex_device_scheduler_heating.clone(),
1437                    &mut channels.heating,
1438                    &mut heating_stats_transfer,
1439                    &mut heating_set_value_updater,
1440                    heating_mutexes,
1441                    sql_interface_heating_stats,
1442                );
1443
1444                // send Quit signal to mock threads because the test object has terminated
1445                let _ =
1446                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
1447                let _ =
1448                    tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
1449
1450                println!("* [Heating] checking reaction to medium temperature succeeded.");
1451            })
1452            .unwrap();
1453
1454        join_handle_mock_schedule_check
1455            .join()
1456            .expect("Mock schedule check did not finish.");
1457        join_handle_mock_relay_manager
1458            .join()
1459            .expect("Mock relay manager thread did not finish.");
1460        join_handle_test_environment
1461            .join()
1462            .expect("Test environment thread did not finish.");
1463        join_handle_test_object
1464            .join()
1465            .expect("Test object thread did not finish.");
1466    }
1467
1468    #[test]
1469    // Test case checks if the Quit command is correctly processed
1470    // - application shall switch off the heater before exiting
1471    // The test case uses test database #14.
1472    pub fn test_heating_with_pending_quit_terminate_command() {
1473        let sleep_duration_100_millis = Duration::from_millis(100);
1474        let spin_sleeper = SpinSleeper::default();
1475
1476        let (mut config, mut heating, sql_interface, sql_interface_heating_stats) =
1477            prepare_heating_tests_mock_midnight_calculator(3, 14, None);
1478
1479        // replacement values are used to initialize the mutexes
1480        config.sensor_manager.replacement_value_water_temperature = 25.0;
1481        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
1482
1483        let mut channels = Channels::new_for_test();
1484
1485        let start_time = Instant::now();
1486
1487        // Mutex for tank level switch signals
1488        let mutex_tank_level_switch_signals =
1489            Arc::new(Mutex::new(TankLevelSwitchSignals::new(true, true, false)));
1490
1491        // thread for mock schedule check
1492        let join_handle_mock_schedule_check = thread::Builder::new()
1493            .name("mock_schedule_check".to_string())
1494            .spawn(move || {
1495                mock_schedule_check(
1496                    &mut channels.schedule_check.tx_schedule_check_to_heating,
1497                    &mut channels.schedule_check.rx_schedule_check_from_heating,
1498                    None,
1499                    true,
1500                );
1501            })
1502            .unwrap();
1503
1504        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
1505
1506        // thread for mock relay manager
1507        let join_handle_mock_relay_manager = thread::Builder::new()
1508            .name("mock_relay_manager".to_string())
1509            .spawn(move || {
1510                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
1511                    &mut channels.relay_manager.tx_relay_manager_to_heating,
1512                    &mut channels.relay_manager.rx_relay_manager_from_heating,
1513                );
1514                println!(
1515                    "Mock relay manager received {} commands.",
1516                    actuation_events.len()
1517                );
1518                assert_eq!(actuation_events.len(), 1);
1519                let actuation_event = actuation_events.pop().unwrap();
1520                assert_eq!(
1521                    actuation_event.command,
1522                    InternalCommand::SwitchOff(AquariumDevice::Heater)
1523                );
1524                mock_actuator_states.check_terminal_condition_heating();
1525            })
1526            .unwrap();
1527
1528        // thread for controlling duration of test run
1529        let join_handle_test_environment = thread::Builder::new()
1530            .name("test_environment".to_string())
1531            .spawn(move || {
1532                let _ = channels
1533                    .signal_handler
1534                    .send_to_heating(InternalCommand::Quit);
1535                channels.signal_handler.receive_from_heating().unwrap();
1536                let _ = channels
1537                    .signal_handler
1538                    .send_to_heating(InternalCommand::Terminate);
1539            })
1540            .unwrap();
1541
1542        spin_sleeper.sleep(sleep_duration_100_millis);
1543
1544        // thread for the test object
1545        let join_handle_test_object = thread::Builder::new()
1546            .name("test_object".to_string())
1547            .spawn(move || {
1548                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1549                let mut tx_heating_to_schedule_check_for_test_case_finish =
1550                    channels.heating.tx_heating_to_schedule_check.clone();
1551                let mut tx_heating_to_relay_manager_for_test_case_finish =
1552                    channels.heating.tx_heating_to_relay_manager.clone();
1553
1554                let mut heating_stats_transfer =
1555                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
1556                let mut heating_set_value_updater = MockSqlInterfaceHeatingSetVals::new(false, None, None, None);
1557
1558                let heating_mutexes = HeatingMutexes {
1559                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(&config.sensor_manager))),
1560                    mutex_tank_level_switch_signals,
1561                    mutex_heating_status: Arc::new(Mutex::new(false)),
1562                };
1563
1564                heating.execute(
1565                    mutex_device_scheduler_heating.clone(),
1566                    &mut channels.heating,
1567                    &mut heating_stats_transfer,
1568                    &mut heating_set_value_updater,
1569                    heating_mutexes,
1570                    sql_interface_heating_stats
1571                );
1572
1573                // send Quit signal to mock threads because the test object has terminated
1574                let _ =
1575                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
1576                let _ = tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
1577
1578                // check if heater actuation has taken place
1579                let mutex_data = mutex_device_scheduler_heating.lock().unwrap();
1580                assert_eq!(*mutex_data, 1);
1581            let finish_time = Instant::now();
1582            let execution_duration = finish_time.duration_since(start_time);
1583            assert_le!(execution_duration.as_millis(), 650);
1584            println!(
1585                "* [Heating] checking heating execution with pending quit/terminate command (time limit) succeeded."
1586            );
1587        })
1588        .unwrap();
1589
1590        join_handle_mock_schedule_check
1591            .join()
1592            .expect("Mock schedule check did not finish.");
1593        join_handle_mock_relay_manager
1594            .join()
1595            .expect("Mock relay manager thread did not finish.");
1596        join_handle_test_environment
1597            .join()
1598            .expect("Test environment thread did not finish.");
1599        join_handle_test_object
1600            .join()
1601            .expect("Test object thread did not finish.");
1602    }
1603
1604    #[test]
1605    // Test case checks if waiting for midnight and statistical calculations are computed correctly.
1606    // Test case uses test database #15.
1607    pub fn test_heating_wait_for_midnight_calc_stats_heater_on() {
1608        let sleep_duration_100_millis = Duration::from_millis(100);
1609        let spin_sleeper = SpinSleeper::default();
1610
1611        let (mut config, mut heating, sql_interface, mut sql_interface_heating_stats) =
1612            prepare_heating_tests_mock_midnight_calculator(3, 15, None);
1613
1614        // replacement values are used to initialize the mutexes
1615        config.sensor_manager.replacement_value_water_temperature = 20.0;
1616        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
1617
1618        let mut channels = Channels::new_for_test();
1619
1620        // Mutex for tank level switch signals
1621        let mutex_tank_level_switch_signals =
1622            Arc::new(Mutex::new(TankLevelSwitchSignals::new(true, true, false)));
1623
1624        // get initial data for calculation of reference value
1625        let initial_heating_stats_entry = sql_interface_heating_stats
1626            .get_single_heating_stats_from_database()
1627            .unwrap_or_else(|_| {
1628                // database responded with an empty response - let's create an empty entry
1629                HeatingStatsEntry {
1630                    date: Local::now().date_naive(),
1631                    energy: 0.0,
1632                    ambient_temperature_average_value: 0.0,
1633                    ambient_temperature_average_counter: 0,
1634                    water_temperature_average_value: 0.0,
1635                    water_temperature_average_counter: 0,
1636                    heating_control_runtime: 0,
1637                }
1638            });
1639
1640        // thread for mock schedule check
1641        let join_handle_mock_schedule_check = thread::Builder::new()
1642            .name("mock_schedule_check".to_string())
1643            .spawn(move || {
1644                mock_schedule_check(
1645                    &mut channels.schedule_check.tx_schedule_check_to_heating,
1646                    &mut channels.schedule_check.rx_schedule_check_from_heating,
1647                    None,
1648                    true,
1649                );
1650            })
1651            .unwrap();
1652
1653        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
1654
1655        // thread for mock relay manager
1656        let join_handle_mock_relay_manager = thread::Builder::new()
1657            .name("mock_relay_manager".to_string())
1658            .spawn(move || {
1659                mock_relay_manager(
1660                    &mut channels.relay_manager.tx_relay_manager_to_heating,
1661                    &mut channels.relay_manager.rx_relay_manager_from_heating,
1662                );
1663            })
1664            .unwrap();
1665
1666        // thread for controlling duration of test run
1667        let join_handle_test_environment = thread::Builder::new()
1668            .name("test_environment".to_string())
1669            .spawn(move || {
1670                let sleep_duration = Duration::from_secs(4);
1671                let spin_sleeper = SpinSleeper::default();
1672                spin_sleeper.sleep(sleep_duration);
1673                let _ = channels
1674                    .signal_handler
1675                    .send_to_heating(InternalCommand::Quit);
1676                channels.signal_handler.receive_from_heating().unwrap();
1677                let _ = channels
1678                    .signal_handler
1679                    .send_to_heating(InternalCommand::Terminate);
1680            })
1681            .unwrap();
1682
1683        spin_sleeper.sleep(sleep_duration_100_millis);
1684
1685        // thread for the test object
1686        let join_handle_test_object = thread::Builder::new()
1687            .name("test_object".to_string())
1688            .spawn(move || {
1689                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1690                let mut tx_heating_to_schedule_check_for_test_case_finish =
1691                    channels.heating.tx_heating_to_schedule_check.clone();
1692                let mut tx_heating_to_relay_manager_for_test_case_finish =
1693                    channels.heating.tx_heating_to_relay_manager.clone();
1694
1695                let mut heating_stats_transfer =
1696                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
1697                let mut heating_set_value_updater = MockSqlInterfaceHeatingSetVals::new(false, None, None, None);
1698
1699                let heating_mutexes = HeatingMutexes {
1700                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(&config.sensor_manager))),
1701                    mutex_tank_level_switch_signals,
1702                    mutex_heating_status: Arc::new(Mutex::new(false)),
1703                };
1704
1705                heating.execute(
1706                    mutex_device_scheduler_heating.clone(),
1707                    &mut channels.heating,
1708                    &mut heating_stats_transfer,
1709                    &mut heating_set_value_updater,
1710                    heating_mutexes,
1711                    sql_interface_heating_stats
1712                );
1713
1714                // send Quit signal to mock threads because the test object has terminated
1715                let _ =
1716                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
1717                let _ = tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
1718
1719                println!(
1720                    "test_heating_wait_for_midnight_calc_stats_heater_on (previous): {}",
1721                    heating_stats_transfer.heating_stats_entry_previous,
1722                );
1723                assert_eq!(
1724                    heating_stats_transfer
1725                        .heating_stats_entry_previous
1726                        .heating_control_runtime
1727                        - initial_heating_stats_entry.heating_control_runtime,
1728                    3
1729                );
1730                assert_le!(
1731                    0.16666666,
1732                    heating_stats_transfer.heating_stats_entry_previous.energy
1733                );
1734                assert_ge!(
1735                    0.16666667,
1736                    heating_stats_transfer.heating_stats_entry_previous.energy
1737                );
1738                assert_le!(
1739                    23.999,
1740                    heating_stats_transfer
1741                        .heating_stats_entry_previous
1742                        .ambient_temperature_average_value
1743                );
1744                assert_ge!(
1745                    24.001,
1746                    heating_stats_transfer
1747                        .heating_stats_entry_previous
1748                        .ambient_temperature_average_value
1749                );
1750                assert_le!(
1751                    19.999,
1752                    heating_stats_transfer
1753                        .heating_stats_entry_previous
1754                        .water_temperature_average_value
1755                );
1756                assert_ge!(
1757                    20.001,
1758                    heating_stats_transfer
1759                        .heating_stats_entry_previous
1760                        .water_temperature_average_value
1761                );
1762                assert_eq!(
1763                    heating_stats_transfer
1764                        .heating_stats_entry_previous
1765                        .ambient_temperature_average_counter
1766                        - initial_heating_stats_entry.ambient_temperature_average_counter,
1767                    3
1768                );
1769                assert_eq!(
1770                    heating_stats_transfer
1771                        .heating_stats_entry_previous
1772                        .water_temperature_average_counter
1773                        - initial_heating_stats_entry.water_temperature_average_counter,
1774                    3
1775                );
1776                println!(
1777                    "test_heating_wait_for_midnight_calc_stats_heater_on (current): {}",
1778                    heating_stats_transfer.heating_stats_entry,
1779                );
1780                assert_eq!(
1781                    heating_stats_transfer
1782                        .heating_stats_entry
1783                        .heating_control_runtime
1784                        - initial_heating_stats_entry.heating_control_runtime,
1785                    1
1786                );
1787                assert_le!(0.0555, heating_stats_transfer.heating_stats_entry.energy);
1788                assert_ge!(0.0556, heating_stats_transfer.heating_stats_entry.energy);
1789                assert_le!(
1790                    23.999,
1791                    heating_stats_transfer
1792                        .heating_stats_entry
1793                        .ambient_temperature_average_value
1794                );
1795                assert_ge!(
1796                    24.001,
1797                    heating_stats_transfer
1798                        .heating_stats_entry
1799                        .ambient_temperature_average_value
1800                );
1801                assert_le!(
1802                    19.999,
1803                    heating_stats_transfer
1804                        .heating_stats_entry
1805                        .water_temperature_average_value
1806                );
1807                assert_ge!(
1808                    20.001,
1809                    heating_stats_transfer
1810                        .heating_stats_entry
1811                        .water_temperature_average_value
1812                );
1813                assert_eq!(
1814                    heating_stats_transfer
1815                        .heating_stats_entry
1816                        .ambient_temperature_average_counter
1817                        - initial_heating_stats_entry.ambient_temperature_average_counter,
1818                    1
1819                );
1820                assert_eq!(
1821                    heating_stats_transfer
1822                        .heating_stats_entry
1823                        .water_temperature_average_counter
1824                        - initial_heating_stats_entry.water_temperature_average_counter,
1825                    1
1826                );
1827                println!("* [Heating] checking heating waiting for midnight and stats calculation with heater on succeeded.");
1828            })
1829            .unwrap();
1830
1831        join_handle_mock_schedule_check
1832            .join()
1833            .expect("Mock schedule check did not finish.");
1834        join_handle_mock_relay_manager
1835            .join()
1836            .expect("Mock relay manager thread did not finish.");
1837        join_handle_test_environment
1838            .join()
1839            .expect("Test environment thread did not finish.");
1840        join_handle_test_object
1841            .join()
1842            .expect("Test object thread did not finish.");
1843    }
1844
1845    #[test]
1846    // Test case checks if waiting for midnight and statistical calculations are computed correctly.
1847    // Test case uses test database #16.
1848    pub fn test_heating_wait_for_midnight_calc_stats_heater_off() {
1849        let sleep_duration_100_millis = Duration::from_millis(100);
1850        let spin_sleeper = SpinSleeper::default();
1851
1852        let (mut config, mut heating, sql_interface, mut sql_interface_heating_stats) =
1853            prepare_heating_tests_mock_midnight_calculator(3, 16, None);
1854
1855        // replacement values are used to initialize the mutexes
1856        config.sensor_manager.replacement_value_water_temperature = 30.0;
1857        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
1858
1859        let mut channels = Channels::new_for_test();
1860
1861        // get initial data for calculation of reference value
1862        let initial_heating_stats_entry = sql_interface_heating_stats
1863            .get_single_heating_stats_from_database()
1864            .unwrap_or_else(|_| {
1865                // database responded with an empty response - let's create an empty entry
1866                HeatingStatsEntry {
1867                    date: Local::now().date_naive(),
1868                    energy: 0.0,
1869                    ambient_temperature_average_value: 0.0,
1870                    ambient_temperature_average_counter: 0,
1871                    water_temperature_average_value: 0.0,
1872                    water_temperature_average_counter: 0,
1873                    heating_control_runtime: 0,
1874                }
1875            });
1876
1877        // thread for mock schedule check
1878        let join_handle_mock_schedule_check = thread::Builder::new()
1879            .name("mock_schedule_check".to_string())
1880            .spawn(move || {
1881                mock_schedule_check(
1882                    &mut channels.schedule_check.tx_schedule_check_to_heating,
1883                    &mut channels.schedule_check.rx_schedule_check_from_heating,
1884                    None,
1885                    true,
1886                );
1887            })
1888            .unwrap();
1889
1890        // Mutex for tank level switch signals
1891        let mutex_tank_level_switch_signals =
1892            Arc::new(Mutex::new(TankLevelSwitchSignals::new(true, true, false)));
1893
1894        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
1895
1896        // thread for mock relay manager
1897        let join_handle_mock_relay_manager = thread::Builder::new()
1898            .name("mock_relay_manager".to_string())
1899            .spawn(move || {
1900                mock_relay_manager(
1901                    &mut channels.relay_manager.tx_relay_manager_to_heating,
1902                    &mut channels.relay_manager.rx_relay_manager_from_heating,
1903                );
1904            })
1905            .unwrap();
1906
1907        // thread for controlling duration of test run
1908        let join_handle_test_environment = thread::Builder::new()
1909            .name("test_environment".to_string())
1910            .spawn(move || {
1911                let sleep_duration = Duration::from_secs(4);
1912                let spin_sleeper = SpinSleeper::default();
1913                spin_sleeper.sleep(sleep_duration);
1914                let _ = channels
1915                    .signal_handler
1916                    .send_to_heating(InternalCommand::Quit);
1917                channels.signal_handler.receive_from_heating().unwrap();
1918                let _ = channels
1919                    .signal_handler
1920                    .send_to_heating(InternalCommand::Terminate);
1921            })
1922            .unwrap();
1923
1924        spin_sleeper.sleep(sleep_duration_100_millis);
1925
1926        // thread for the test object
1927        let join_handle_test_object = thread::Builder::new()
1928            .name("test_object".to_string())
1929            .spawn(move || {
1930                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1931                let mut tx_heating_to_schedule_check_for_test_case_finish =
1932                    channels.heating.tx_heating_to_schedule_check.clone();
1933                let mut tx_heating_to_relay_manager_for_test_case_finish =
1934                    channels.heating.tx_heating_to_relay_manager.clone();
1935
1936                let mut heating_stats_transfer =
1937                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
1938                let mut heating_set_value_updater = MockSqlInterfaceHeatingSetVals::new(false, None, None, None);
1939
1940                let heating_mutexes = HeatingMutexes {
1941                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(&config.sensor_manager))),
1942                    mutex_tank_level_switch_signals,
1943                    mutex_heating_status: Arc::new(Mutex::new(false)),
1944                };
1945
1946                heating.execute(
1947                    mutex_device_scheduler_heating.clone(),
1948                    &mut channels.heating,
1949                    &mut heating_stats_transfer,
1950                    &mut heating_set_value_updater,
1951                    heating_mutexes,
1952                    sql_interface_heating_stats
1953                );
1954
1955                // send Quit signal to mock threads because the test object has terminated
1956                let _ =
1957                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
1958                let _ = tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
1959
1960                println!(
1961                    "test_heating_wait_for_midnight_calc_stats_heater_on (previous): {}",
1962                    heating_stats_transfer.heating_stats_entry_previous,
1963                );
1964                assert_eq!(
1965                    heating_stats_transfer
1966                        .heating_stats_entry_previous
1967                        .heating_control_runtime
1968                        - initial_heating_stats_entry.heating_control_runtime,
1969                    3
1970                );
1971                assert_le!(
1972                    -0.001,
1973                    heating_stats_transfer.heating_stats_entry_previous.energy
1974                );
1975                assert_ge!(
1976                    0.001,
1977                    heating_stats_transfer.heating_stats_entry_previous.energy
1978                );
1979                assert_le!(
1980                    23.999,
1981                    heating_stats_transfer
1982                        .heating_stats_entry_previous
1983                        .ambient_temperature_average_value
1984                );
1985                assert_ge!(
1986                    24.001,
1987                    heating_stats_transfer
1988                        .heating_stats_entry_previous
1989                        .ambient_temperature_average_value
1990                );
1991                assert_le!(
1992                    29.999,
1993                    heating_stats_transfer
1994                        .heating_stats_entry_previous
1995                        .water_temperature_average_value
1996                );
1997                assert_ge!(
1998                    30.001,
1999                    heating_stats_transfer
2000                        .heating_stats_entry_previous
2001                        .water_temperature_average_value
2002                );
2003                assert_eq!(
2004                    heating_stats_transfer
2005                        .heating_stats_entry_previous
2006                        .ambient_temperature_average_counter
2007                        - initial_heating_stats_entry.ambient_temperature_average_counter,
2008                    3
2009                );
2010                assert_eq!(
2011                    heating_stats_transfer
2012                        .heating_stats_entry_previous
2013                        .water_temperature_average_counter
2014                        - initial_heating_stats_entry.water_temperature_average_counter,
2015                    3
2016                );
2017                println!(
2018                    "test_heating_wait_for_midnight_calc_stats_heater_on (current): {}",
2019                    heating_stats_transfer.heating_stats_entry,
2020                );
2021                assert_eq!(
2022                    heating_stats_transfer
2023                        .heating_stats_entry
2024                        .heating_control_runtime
2025                        - initial_heating_stats_entry.heating_control_runtime,
2026                    1
2027                );
2028                assert_le!(-0.001, heating_stats_transfer.heating_stats_entry.energy);
2029                assert_ge!(0.001, heating_stats_transfer.heating_stats_entry.energy);
2030                assert_le!(
2031                    23.999,
2032                    heating_stats_transfer
2033                        .heating_stats_entry
2034                        .ambient_temperature_average_value
2035                );
2036                assert_ge!(
2037                    24.001,
2038                    heating_stats_transfer
2039                        .heating_stats_entry
2040                        .ambient_temperature_average_value
2041                );
2042                assert_le!(
2043                    29.999,
2044                    heating_stats_transfer
2045                        .heating_stats_entry
2046                        .water_temperature_average_value
2047                );
2048                assert_ge!(
2049                    30.001,
2050                    heating_stats_transfer
2051                        .heating_stats_entry
2052                        .water_temperature_average_value
2053                );
2054                assert_eq!(
2055                    heating_stats_transfer
2056                        .heating_stats_entry
2057                        .ambient_temperature_average_counter
2058                        - initial_heating_stats_entry.ambient_temperature_average_counter,
2059                    1
2060                );
2061                assert_eq!(
2062                    heating_stats_transfer
2063                        .heating_stats_entry
2064                        .water_temperature_average_counter
2065                        - initial_heating_stats_entry.water_temperature_average_counter,
2066                    1
2067                );
2068                println!("* [Heating] checking heating waiting for midnight and stats calculation with heater on succeeded.");
2069            })
2070            .unwrap();
2071
2072        join_handle_mock_schedule_check
2073            .join()
2074            .expect("Mock schedule check did not finish.");
2075        join_handle_mock_relay_manager
2076            .join()
2077            .expect("Mock relay manager thread did not finish.");
2078        join_handle_test_environment
2079            .join()
2080            .expect("Test environment thread did not finish.");
2081        join_handle_test_object
2082            .join()
2083            .expect("Test object thread did not finish.");
2084    }
2085
2086    #[test]
2087    // Test case checks if a negative schedule checker result is processed correctly.
2088    // Test case uses test database #17.
2089    pub fn test_heating_blocked_by_schedule() {
2090        let sleep_duration_100_millis = Duration::from_millis(100);
2091        let spin_sleeper = SpinSleeper::default();
2092
2093        let (mut config, mut heating, sql_interface, sql_interface_heating_stats) =
2094            prepare_heating_tests_mock_midnight_calculator(3600, 17, None);
2095
2096        // replacement values are used to initialize the mutexes
2097        config.sensor_manager.replacement_value_water_temperature = 20.0;
2098        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
2099
2100        let mut channels = Channels::new_for_test();
2101
2102        // Mutex for tank level switch signals
2103        let mutex_tank_level_switch_signals =
2104            Arc::new(Mutex::new(TankLevelSwitchSignals::new(true, true, false)));
2105
2106        // thread for mock schedule check
2107        let join_handle_mock_schedule_check = thread::Builder::new()
2108            .name("mock_schedule_check".to_string())
2109            .spawn(move || {
2110                mock_schedule_check(
2111                    &mut channels.schedule_check.tx_schedule_check_to_heating,
2112                    &mut channels.schedule_check.rx_schedule_check_from_heating,
2113                    None,
2114                    false,
2115                );
2116            })
2117            .unwrap();
2118
2119        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
2120
2121        // thread for mock relay manager - includes assertions
2122        let join_handle_mock_relay_manager = thread::Builder::new()
2123            .name("mock_relay_manager".to_string())
2124            .spawn(move || {
2125                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
2126                    &mut channels.relay_manager.tx_relay_manager_to_heating,
2127                    &mut channels.relay_manager.rx_relay_manager_from_heating,
2128                );
2129                assert_eq!(actuation_events.len(), 1);
2130                let actuation_event = actuation_events.pop().unwrap();
2131                assert_eq!(
2132                    actuation_event.command,
2133                    InternalCommand::SwitchOff(AquariumDevice::Heater)
2134                );
2135                mock_actuator_states.check_terminal_condition_heating();
2136            })
2137            .unwrap();
2138
2139        // thread for controlling duration of test run
2140        let join_handle_test_environment = thread::Builder::new()
2141            .name("test_environment".to_string())
2142            .spawn(move || {
2143                let sleep_duration_ten_seconds = Duration::from_secs(10);
2144                let spin_sleeper = SpinSleeper::default();
2145                spin_sleeper.sleep(sleep_duration_ten_seconds);
2146                let _ = channels
2147                    .signal_handler
2148                    .send_to_heating(InternalCommand::Quit);
2149                channels.signal_handler.receive_from_heating().unwrap();
2150                let _ = channels
2151                    .signal_handler
2152                    .send_to_heating(InternalCommand::Terminate);
2153            })
2154            .unwrap();
2155
2156        // make sure all mock threads are running when instantiating the test object
2157        spin_sleeper.sleep(sleep_duration_100_millis);
2158
2159        // thread for the test object
2160        let join_handle_test_object = thread::Builder::new()
2161            .name("test_object".to_string())
2162            .spawn(move || {
2163                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
2164                let mut tx_heating_to_schedule_check_for_test_case_finish =
2165                    channels.heating.tx_heating_to_schedule_check.clone();
2166                let mut tx_heating_to_relay_manager_for_test_case_finish =
2167                    channels.heating.tx_heating_to_relay_manager.clone();
2168
2169                let mut heating_stats_transfer =
2170                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
2171                let mut heating_set_value_updater =
2172                    MockSqlInterfaceHeatingSetVals::new(false, None, None, None);
2173
2174                let heating_mutexes = HeatingMutexes {
2175                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(
2176                        &config.sensor_manager,
2177                    ))),
2178                    mutex_tank_level_switch_signals,
2179                    mutex_heating_status: Arc::new(Mutex::new(false)),
2180                };
2181
2182                heating.execute(
2183                    mutex_device_scheduler_heating.clone(),
2184                    &mut channels.heating,
2185                    &mut heating_stats_transfer,
2186                    &mut heating_set_value_updater,
2187                    heating_mutexes,
2188                    sql_interface_heating_stats,
2189                );
2190
2191                // send Quit signal to mock threads because the test object has terminated
2192                let _ =
2193                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
2194                let _ =
2195                    tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
2196                println!("* [Heating] checking if schedule checker can block actuation.");
2197            })
2198            .unwrap();
2199
2200        join_handle_mock_schedule_check
2201            .join()
2202            .expect("Mock schedule check did not finish.");
2203        join_handle_mock_relay_manager
2204            .join()
2205            .expect("Mock relay manager thread did not finish.");
2206        join_handle_test_environment
2207            .join()
2208            .expect("Test environment thread did not finish.");
2209        join_handle_test_object
2210            .join()
2211            .expect("Test object thread did not finish.");
2212    }
2213
2214    #[test]
2215    // Test case checks if component protection is executed.
2216    // Test case uses test database #18.
2217    pub fn test_heating_component_protection_when_tank_level_low() {
2218        let sleep_duration_100_millis = Duration::from_millis(100);
2219        let spin_sleeper = SpinSleeper::default();
2220
2221        let (mut config, mut heating, sql_interface, sql_interface_heating_stats) =
2222            prepare_heating_tests_mock_midnight_calculator(3, 18, None);
2223        // replacement values are used to initialize the mutexes
2224        config.sensor_manager.replacement_value_water_temperature = 20.0;
2225        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
2226
2227        let mut channels = Channels::new_for_test();
2228
2229        // thread for mock schedule check
2230        let join_handle_mock_schedule_check = thread::Builder::new()
2231            .name("mock_schedule_check".to_string())
2232            .spawn(move || {
2233                mock_schedule_check(
2234                    &mut channels.schedule_check.tx_schedule_check_to_heating,
2235                    &mut channels.schedule_check.rx_schedule_check_from_heating,
2236                    None,
2237                    true,
2238                );
2239            })
2240            .unwrap();
2241
2242        // Mutex for tank level switch signals
2243        let mutex_tank_level_switch_signals =
2244            Arc::new(Mutex::new(TankLevelSwitchSignals::new(false, false, false)));
2245
2246        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
2247
2248        // thread for mock relay manager - includes assertions
2249        let join_handle_mock_relay_manager = thread::Builder::new()
2250            .name("mock_relay_manager".to_string())
2251            .spawn(move || {
2252                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
2253                    &mut channels.relay_manager.tx_relay_manager_to_heating,
2254                    &mut channels.relay_manager.rx_relay_manager_from_heating,
2255                );
2256                assert_eq!(actuation_events.len(), 2);
2257                let actuation_event = actuation_events.pop().unwrap();
2258                assert_eq!(
2259                    actuation_event.command,
2260                    InternalCommand::SwitchOff(AquariumDevice::Heater)
2261                );
2262                let actuation_event = actuation_events.pop().unwrap();
2263                assert_eq!(
2264                    actuation_event.command,
2265                    InternalCommand::SwitchOn(AquariumDevice::Heater)
2266                );
2267                mock_actuator_states.check_terminal_condition_heating();
2268            })
2269            .unwrap();
2270
2271        // thread for controlling duration of test run
2272        let join_handle_test_environment = thread::Builder::new()
2273            .name("test_environment".to_string())
2274            .spawn(move || {
2275                let sleep_duration_ten_seconds = Duration::from_secs(10);
2276                let spin_sleeper = SpinSleeper::default();
2277                spin_sleeper.sleep(sleep_duration_ten_seconds);
2278                let _ = channels
2279                    .signal_handler
2280                    .send_to_heating(InternalCommand::Quit);
2281                channels.signal_handler.receive_from_heating().unwrap();
2282                let _ = channels
2283                    .signal_handler
2284                    .send_to_heating(InternalCommand::Terminate);
2285            })
2286            .unwrap();
2287
2288        // make sure all mock threads are running when instantiating the test object
2289        spin_sleeper.sleep(sleep_duration_100_millis);
2290
2291        // thread for the test object
2292        let join_handle_test_object = thread::Builder::new()
2293            .name("test_object".to_string())
2294            .spawn(move || {
2295                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
2296                let mut tx_heating_to_schedule_check_for_test_case_finish =
2297                    channels.heating.tx_heating_to_schedule_check.clone();
2298                let mut tx_heating_to_relay_manager_for_test_case_finish =
2299                    channels.heating.tx_heating_to_relay_manager.clone();
2300
2301                let mut heating_stats_transfer =
2302                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
2303                let mut heating_set_value_updater =
2304                    MockSqlInterfaceHeatingSetVals::new(false, None, None, None);
2305
2306                let heating_mutexes = HeatingMutexes {
2307                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(
2308                        &config.sensor_manager,
2309                    ))),
2310                    mutex_tank_level_switch_signals,
2311                    mutex_heating_status: Arc::new(Mutex::new(false)),
2312                };
2313
2314                heating.execute(
2315                    mutex_device_scheduler_heating.clone(),
2316                    &mut channels.heating,
2317                    &mut heating_stats_transfer,
2318                    &mut heating_set_value_updater,
2319                    heating_mutexes,
2320                    sql_interface_heating_stats,
2321                );
2322
2323                // send Quit signal to mock threads because the test object has terminated
2324                let _ =
2325                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
2326                let _ =
2327                    tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
2328
2329                println!("* [Heating] checking if schedule checker can block actuation.");
2330            })
2331            .unwrap();
2332
2333        join_handle_mock_schedule_check
2334            .join()
2335            .expect("Mock schedule check did not finish.");
2336        join_handle_mock_relay_manager
2337            .join()
2338            .expect("Mock relay manager thread did not finish.");
2339        join_handle_test_environment
2340            .join()
2341            .expect("Test environment thread did not finish.");
2342        join_handle_test_object
2343            .join()
2344            .expect("Test object thread did not finish.");
2345    }
2346
2347    #[test]
2348    // Test case runs heating control and triggers inhibition by sending a stop message via the channel.
2349    // After verification that heating is stopped,
2350    // The test case sends the start message via the channel and verifies if heating control
2351    // resumes operation.
2352    // Test case uses test database #19.
2353    pub fn test_messaging_stops_starts_heating() {
2354        let (mut config, mut heating, sql_interface, sql_interface_heating_stats) =
2355            prepare_heating_tests_mock_midnight_calculator(3600, 19, None);
2356
2357        // replacement values are used to initialize the mutexes
2358        config.sensor_manager.replacement_value_water_temperature = 20.0;
2359        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
2360
2361        let mut channels = Channels::new_for_test();
2362
2363        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
2364        let mutex_device_scheduler_test_environment = mutex_device_scheduler_heating.clone();
2365
2366        // thread for mock schedule check
2367        let join_handle_mock_schedule_check = thread::Builder::new()
2368            .name("mock_schedule_check".to_string())
2369            .spawn(move || {
2370                mock_schedule_check(
2371                    &mut channels.schedule_check.tx_schedule_check_to_heating,
2372                    &mut channels.schedule_check.rx_schedule_check_from_heating,
2373                    None,
2374                    true,
2375                );
2376            })
2377            .unwrap();
2378
2379        // Mutex for tank level switch signals
2380        let mutex_tank_level_switch_signals =
2381            Arc::new(Mutex::new(TankLevelSwitchSignals::new(true, true, false)));
2382
2383        // thread for mock relay manager - includes assertions
2384        let join_handle_mock_relay_manager = thread::Builder::new()
2385            .name("mock_relay_manager".to_string())
2386            .spawn(move || {
2387                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
2388                    &mut channels.relay_manager.tx_relay_manager_to_heating,
2389                    &mut channels.relay_manager.rx_relay_manager_from_heating,
2390                );
2391                println!("actuation_events:");
2392                for actuation_event in &actuation_events {
2393                    println!("{}", actuation_event);
2394                }
2395                assert_eq!(actuation_events.len(), 4);
2396                let actuation_event = actuation_events.pop().unwrap();
2397                assert_eq!(
2398                    actuation_event.command,
2399                    InternalCommand::SwitchOff(AquariumDevice::Heater)
2400                );
2401                let actuation_event = actuation_events.pop().unwrap();
2402                assert_eq!(
2403                    actuation_event.command,
2404                    InternalCommand::SwitchOn(AquariumDevice::Heater)
2405                );
2406                let actuation_event = actuation_events.pop().unwrap();
2407                assert_eq!(
2408                    actuation_event.command,
2409                    InternalCommand::SwitchOff(AquariumDevice::Heater)
2410                );
2411                let actuation_event = actuation_events.pop().unwrap();
2412                assert_eq!(
2413                    actuation_event.command,
2414                    InternalCommand::SwitchOn(AquariumDevice::Heater)
2415                );
2416                mock_actuator_states.check_terminal_condition_heating();
2417            })
2418            .unwrap();
2419
2420        // thread for test environment
2421        let join_handle_test_environment = thread::Builder::new()
2422            .name("test_environment".to_string())
2423            .spawn(move || {
2424                let sleep_duration_100_millis = Duration::from_millis(100);
2425                let spin_sleeper = SpinSleeper::default();
2426
2427                // initial wait so that heating control can switch on
2428                for _ in 0..10 {
2429                    spin_sleeper.sleep(sleep_duration_100_millis);
2430                }
2431                // check the initial state of mutex: should be already switched on
2432                assert_eq!(*mutex_device_scheduler_test_environment.lock().unwrap(), 1);
2433
2434                // sending a message requesting stop of heating control
2435                match channels
2436                    .messaging
2437                    .tx_messaging_to_heating
2438                    .send(InternalCommand::Stop)
2439                {
2440                    Ok(()) => { /* do nothing */ }
2441                    Err(e) => {
2442                        panic!(
2443                            "{}: error when sending stop command to test object ({e:?})",
2444                            module_path!()
2445                        );
2446                    }
2447                }
2448
2449                // wait for 1 second: heating control will switch off
2450                for _ in 0..10 {
2451                    spin_sleeper.sleep(sleep_duration_100_millis);
2452                }
2453                // check if heating is actuated
2454                assert_eq!(*mutex_device_scheduler_test_environment.lock().unwrap(), 2);
2455
2456                // sending a message requesting restart of heating control
2457                match channels
2458                    .messaging
2459                    .tx_messaging_to_heating
2460                    .send(InternalCommand::Start)
2461                {
2462                    Ok(()) => { /* do nothing */ }
2463                    Err(e) => {
2464                        panic!(
2465                            "{}: error when sending start command to test object ({e:?})",
2466                            module_path!()
2467                        );
2468                    }
2469                }
2470
2471                // Wait for 1 second. Heating control will switch on
2472                for _ in 0..10 {
2473                    spin_sleeper.sleep(sleep_duration_100_millis);
2474                }
2475                // check if heating is actuated
2476                assert_eq!(*mutex_device_scheduler_test_environment.lock().unwrap(), 3);
2477
2478                // sending a message requesting stop of heating control
2479                match channels
2480                    .messaging
2481                    .tx_messaging_to_heating
2482                    .send(InternalCommand::Stop)
2483                {
2484                    Ok(()) => { /* do nothing */ }
2485                    Err(e) => {
2486                        panic!(
2487                            "{}: error when sending stop command to test object ({e:?})",
2488                            module_path!()
2489                        );
2490                    }
2491                }
2492
2493                // wait for 1 second: Heating control will switch off
2494                for _ in 0..10 {
2495                    spin_sleeper.sleep(sleep_duration_100_millis);
2496                }
2497                // check if heating is actuated
2498                assert_eq!(*mutex_device_scheduler_test_environment.lock().unwrap(), 4);
2499
2500                // requesting heating control to quit
2501                let _ = channels
2502                    .signal_handler
2503                    .send_to_heating(InternalCommand::Quit);
2504                channels.signal_handler.receive_from_heating().unwrap();
2505
2506                // check if heating is not actuated
2507                assert_eq!(*mutex_device_scheduler_test_environment.lock().unwrap(), 4);
2508                let _ = channels
2509                    .signal_handler
2510                    .send_to_heating(InternalCommand::Terminate);
2511            })
2512            .unwrap();
2513
2514        // thread for the test object
2515        let join_handle_test_object = thread::Builder::new()
2516            .name("test_object".to_string())
2517            .spawn(move || {
2518                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
2519                let mut tx_heating_to_schedule_check_for_test_case_finish =
2520                    channels.heating.tx_heating_to_schedule_check.clone();
2521                let mut tx_heating_to_relay_manager_for_test_case_finish =
2522                    channels.heating.tx_heating_to_relay_manager.clone();
2523
2524                let mut heating_stats_transfer =
2525                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
2526                let mut heating_set_value_updater =
2527                    MockSqlInterfaceHeatingSetVals::new(false, None, None, None);
2528
2529                let heating_mutexes = HeatingMutexes {
2530                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(
2531                        &config.sensor_manager,
2532                    ))),
2533                    mutex_tank_level_switch_signals,
2534                    mutex_heating_status: Arc::new(Mutex::new(false)),
2535                };
2536
2537                heating.execute(
2538                    mutex_device_scheduler_heating.clone(),
2539                    &mut channels.heating,
2540                    &mut heating_stats_transfer,
2541                    &mut heating_set_value_updater,
2542                    heating_mutexes,
2543                    sql_interface_heating_stats,
2544                );
2545
2546                // send Quit signal to mock threads because the test object has terminated
2547                let _ =
2548                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
2549                let _ =
2550                    tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
2551
2552                println!("* [Heating] checking if messaging can block and restart actuation.");
2553            })
2554            .unwrap();
2555
2556        join_handle_mock_schedule_check
2557            .join()
2558            .expect("Mock schedule check did not finish.");
2559        join_handle_mock_relay_manager
2560            .join()
2561            .expect("Mock relay manager thread did not finish.");
2562        join_handle_test_environment
2563            .join()
2564            .expect("Test environment thread did not finish.");
2565        join_handle_test_object
2566            .join()
2567            .expect("Test object thread did not finish.");
2568    }
2569
2570    #[test]
2571    // Check if the heater remains switched off with a high temperature initially.
2572    // After a limited time period, increase the set values and observe if the heater is switched on.
2573    // Also, implicitly test if the execute function terminates after receiving Quit and Terminate commands.
2574    // Test case uses test database #50.
2575    pub fn test_heating_with_increased_set_values() {
2576        let sleep_duration_100_millis = Duration::from_millis(100);
2577        let spin_sleeper = SpinSleeper::default();
2578
2579        let (mut config, mut heating, sql_interface, sql_interface_heating_stats) =
2580            prepare_heating_tests_mock_midnight_calculator(3600, 50, None);
2581        // replacement values are used to initialize the mutexes
2582        config.sensor_manager.replacement_value_water_temperature = 30.0;
2583        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
2584
2585        let mut channels = Channels::new_for_test();
2586
2587        // Mutex for tank level switch signals
2588        let mutex_tank_level_switch_signals =
2589            Arc::new(Mutex::new(TankLevelSwitchSignals::new(true, true, false)));
2590
2591        // thread for mock schedule check
2592        let join_handle_mock_schedule_check = thread::Builder::new()
2593            .name("mock_schedule_check".to_string())
2594            .spawn(move || {
2595                mock_schedule_check(
2596                    &mut channels.schedule_check.tx_schedule_check_to_heating,
2597                    &mut channels.schedule_check.rx_schedule_check_from_heating,
2598                    None,
2599                    true,
2600                );
2601            })
2602            .unwrap();
2603
2604        // thread for mock TankLevelSwitch
2605        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
2606
2607        // thread for mock relay manager - includes assertions
2608        let join_handle_mock_relay_manager = thread::Builder::new()
2609            .name("mock_relay_manager".to_string())
2610            .spawn(move || {
2611                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
2612                    &mut channels.relay_manager.tx_relay_manager_to_heating,
2613                    &mut channels.relay_manager.rx_relay_manager_from_heating,
2614                );
2615                assert_eq!(actuation_events.len(), 3);
2616                let actuation_event_3 = actuation_events.pop().unwrap();
2617                let actuation_event_2 = actuation_events.pop().unwrap();
2618                let actuation_event_1 = actuation_events.pop().unwrap();
2619                assert_eq!(
2620                    actuation_event_1.command,
2621                    InternalCommand::SwitchOff(AquariumDevice::Heater)
2622                );
2623                assert_eq!(
2624                    actuation_event_2.command,
2625                    InternalCommand::SwitchOn(AquariumDevice::Heater)
2626                );
2627                assert_eq!(
2628                    actuation_event_3.command,
2629                    InternalCommand::SwitchOff(AquariumDevice::Heater)
2630                );
2631                mock_actuator_states.check_terminal_condition_heating();
2632            })
2633            .unwrap();
2634
2635        // thread for controlling duration of test run
2636        let join_handle_test_environment = thread::Builder::new()
2637            .name("test_environment".to_string())
2638            .spawn(move || {
2639                let sleep_duration_twelve_seconds = Duration::from_secs(12);
2640                let spin_sleeper = SpinSleeper::default();
2641                spin_sleeper.sleep(sleep_duration_twelve_seconds);
2642                let _ = channels
2643                    .signal_handler
2644                    .send_to_heating(InternalCommand::Quit);
2645                channels.signal_handler.receive_from_heating().unwrap();
2646                let _ = channels
2647                    .signal_handler
2648                    .send_to_heating(InternalCommand::Terminate);
2649            })
2650            .unwrap();
2651
2652        // make sure all mock threads are running when instantiating the test object
2653        spin_sleeper.sleep(sleep_duration_100_millis);
2654
2655        // thread for the test object
2656        let join_handle_test_object = thread::Builder::new()
2657            .name("test_object".to_string())
2658            .spawn(move || {
2659                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
2660                let mut tx_heating_to_schedule_check_for_test_case_finish =
2661                    channels.heating.tx_heating_to_schedule_check.clone();
2662                let mut tx_heating_to_relay_manager_for_test_case_finish =
2663                    channels.heating.tx_heating_to_relay_manager.clone();
2664
2665                let mut heating_stats_transfer =
2666                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
2667
2668                // set up the set value updater to actively manipulate the set values
2669                let mut heating_set_value_updater = MockSqlInterfaceHeatingSetVals::new(
2670                    true,
2671                    Some(50.0),
2672                    Some(40.0),
2673                    Some(Duration::from_secs(10)),
2674                );
2675
2676                let heating_mutexes = HeatingMutexes {
2677                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(
2678                        &config.sensor_manager,
2679                    ))),
2680                    mutex_tank_level_switch_signals,
2681                    mutex_heating_status: Arc::new(Mutex::new(false)),
2682                };
2683
2684                heating.execute(
2685                    mutex_device_scheduler_heating.clone(),
2686                    &mut channels.heating,
2687                    &mut heating_stats_transfer,
2688                    &mut heating_set_value_updater,
2689                    heating_mutexes,
2690                    sql_interface_heating_stats,
2691                );
2692
2693                // send Quit signal to mock threads because the test object has terminated
2694                let _ =
2695                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
2696                let _ =
2697                    tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
2698
2699                println!("* [Heating] checking reaction to high temperature succeeded.");
2700            })
2701            .unwrap();
2702
2703        join_handle_mock_schedule_check
2704            .join()
2705            .expect("Mock schedule check did not finish.");
2706        join_handle_mock_relay_manager
2707            .join()
2708            .expect("Mock relay manager thread did not finish.");
2709        join_handle_test_environment
2710            .join()
2711            .expect("Test environment thread did not finish.");
2712        join_handle_test_object
2713            .join()
2714            .expect("Test object thread did not finish.");
2715    }
2716
2717    #[test]
2718    // Check if the heater is switched on and is not switched off with low temperature.
2719    // Also, implicitly test if the execute function terminates after receiving Quit and Terminate commands.
2720    // Check if the heater is switched off after sending Quit command and before sending Terminate command.
2721    // Test case uses test database #12.
2722    // Additionally, mutex for heater status is blocked by test environment
2723    pub fn test_heating_with_measured_temperature_low_with_mutex_blocked() {
2724        let sleep_duration_100_millis = Duration::from_millis(100);
2725        let spin_sleeper = SpinSleeper::default();
2726
2727        let (mut config, mut heating, sql_interface, sql_interface_heating_stats) =
2728            prepare_heating_tests_mock_midnight_calculator(3600, 12, None);
2729
2730        // replacement values are used to initialize the mutexes
2731        config.sensor_manager.replacement_value_water_temperature = 20.0;
2732        config.sensor_manager.replacement_value_ambient_temperature = 24.0;
2733
2734        let mut channels = Channels::new_for_test();
2735
2736        // Mutex for tank level switch signals
2737        let mutex_tank_level_switch_signals =
2738            Arc::new(Mutex::new(TankLevelSwitchSignals::new(true, true, false)));
2739
2740        // thread for mock schedule check
2741        let join_handle_mock_schedule_check = thread::Builder::new()
2742            .name("mock_schedule_check".to_string())
2743            .spawn(move || {
2744                mock_schedule_check(
2745                    &mut channels.schedule_check.tx_schedule_check_to_heating,
2746                    &mut channels.schedule_check.rx_schedule_check_from_heating,
2747                    None,
2748                    true,
2749                );
2750            })
2751            .unwrap();
2752
2753        let mutex_device_scheduler_heating = Arc::new(Mutex::new(0));
2754        let mutex_heating_status = Arc::new(Mutex::new(false));
2755        let mutex_heating_status_clone_for_test_environment = mutex_heating_status.clone();
2756
2757        // thread for mock relay manager - includes assertions
2758        let join_handle_mock_relay_manager = thread::Builder::new()
2759            .name("mock_relay_manager".to_string())
2760            .spawn(move || {
2761                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
2762                    &mut channels.relay_manager.tx_relay_manager_to_heating,
2763                    &mut channels.relay_manager.rx_relay_manager_from_heating,
2764                );
2765                assert_eq!(actuation_events.len(), 2);
2766                let last_actuation_event = actuation_events.pop().unwrap();
2767                assert_eq!(
2768                    last_actuation_event.command,
2769                    InternalCommand::SwitchOff(AquariumDevice::Heater)
2770                );
2771                let first_actuation_event = actuation_events.pop().unwrap();
2772                assert_eq!(
2773                    first_actuation_event.command,
2774                    InternalCommand::SwitchOn(AquariumDevice::Heater)
2775                );
2776                mock_actuator_states.check_terminal_condition_heating();
2777            })
2778            .unwrap();
2779
2780        // thread for controlling duration of test run
2781        let join_handle_test_environment = thread::Builder::new()
2782            .name("test_environment".to_string())
2783            .spawn(move || {
2784                let sleep_duration_one_second = Duration::from_secs(1);
2785                let spin_sleeper = SpinSleeper::default();
2786                for i in 0..10 {
2787                    if i % 2 == 0 {
2788                        spin_sleeper.sleep(sleep_duration_one_second);
2789                    } else {
2790                        // block mutex on purpose
2791                        {
2792                            match mutex_heating_status_clone_for_test_environment.lock() {
2793                                Ok(_) => {
2794                                    spin_sleeper.sleep(sleep_duration_one_second);
2795                                }
2796                                Err(_) => {
2797                                    // Do nothing
2798                                }
2799                            }
2800                        }
2801                    }
2802                }
2803                let _ = channels
2804                    .signal_handler
2805                    .send_to_heating(InternalCommand::Quit);
2806                channels.signal_handler.receive_from_heating().unwrap();
2807                let _ = channels
2808                    .signal_handler
2809                    .send_to_heating(InternalCommand::Terminate);
2810            })
2811            .unwrap();
2812
2813        // make sure all mock threads are running when instantiating the test object
2814        spin_sleeper.sleep(sleep_duration_100_millis);
2815
2816        // thread for the test object
2817        let join_handle_test_object = thread::Builder::new()
2818            .name("test_object".to_string())
2819            .spawn(move || {
2820                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
2821                let mut tx_heating_to_schedule_check_for_test_case_finish =
2822                    channels.heating.tx_heating_to_schedule_check.clone();
2823                let mut tx_heating_to_relay_manager_for_test_case_finish =
2824                    channels.heating.tx_heating_to_relay_manager.clone();
2825
2826                let mut heating_stats_transfer =
2827                    HeatingStatsMockDataTransfer::new(&mut sql_interface.get_connection().unwrap());
2828                let mut heating_set_value_updater =
2829                    MockSqlInterfaceHeatingSetVals::new(false, None, None, None);
2830
2831                let heating_mutexes = HeatingMutexes {
2832                    mutex_sensor_manager_signals: Arc::new(Mutex::new(SensorManagerSignals::new(
2833                        &config.sensor_manager,
2834                    ))),
2835                    mutex_tank_level_switch_signals,
2836                    mutex_heating_status
2837                };
2838
2839                heating.execute(
2840                    mutex_device_scheduler_heating.clone(),
2841                    &mut channels.heating,
2842                    &mut heating_stats_transfer,
2843                    &mut heating_set_value_updater,
2844                    heating_mutexes,
2845                    sql_interface_heating_stats
2846                );
2847
2848                // send Quit signal to mock threads because the test object has terminated
2849                let _ =
2850                    tx_heating_to_schedule_check_for_test_case_finish.send(InternalCommand::Quit);
2851                let _ =
2852                    tx_heating_to_relay_manager_for_test_case_finish.send(InternalCommand::Quit);
2853
2854                assert_eq!(heating.mutex_access_duration_exceeded, true);
2855
2856                println!("* [Heating] checking reaction to low temperature with mutex blocked succeeded.");
2857            })
2858            .unwrap();
2859
2860        join_handle_mock_schedule_check
2861            .join()
2862            .expect("Mock schedule check did not finish.");
2863        join_handle_mock_relay_manager
2864            .join()
2865            .expect("Mock relay manager thread did not finish.");
2866        join_handle_test_environment
2867            .join()
2868            .expect("Test environment thread did not finish.");
2869        join_handle_test_object
2870            .join()
2871            .expect("Test object thread did not finish.");
2872    }
2873}