2026-04-02 16:00:27 +02:00
// SPDX-License-Identifier: GPL-3.0-or-later
2026-04-02 16:25:49 +02:00
// Copyright (C) 2025 Andrea Mazzoleni
2025-09-21 19:54:10 +02:00
# include "portable.h"
# include "thermal.h"
# include "state.h"
# include "io.h"
# include <math.h>
struct snapraid_thermal * thermal_alloc ( uint64_t dev , const char * name )
{
struct snapraid_thermal * thermal = malloc_nofail ( sizeof ( struct snapraid_thermal ) ) ;
thermal - > device = dev ;
thermal - > latest_temperature = 0 ;
thermal - > count = 0 ;
pathcpy ( thermal - > name , sizeof ( thermal - > name ) , name ) ;
return thermal ;
}
void thermal_free ( struct snapraid_thermal * thermal )
{
free ( thermal ) ;
}
2025-10-26 18:06:08 +01:00
/*
* Fit exponential heating model to data using least squares
*/
struct snapraid_thermal_params fit_thermal_model ( const struct snapraid_thermal_point * points , int n_points , double t_ambient )
2025-09-21 19:54:10 +02:00
{
struct snapraid_thermal_params model ;
double t_steady_try ;
double k_try ;
memset ( & model , 0 , sizeof ( model ) ) ;
model . t_ambient = t_ambient ;
2025-10-17 09:12:21 +02:00
/* at least four points to have a result */
if ( n_points < 4 )
2025-09-21 19:54:10 +02:00
return model ;
double last_temp = points [ n_points - 1 ] . temperature ;
/* iterative refinement to find best k_heat and t_steady */
double best_error = 1e10 ;
double best_k = 0 ;
double best_t_steady = 0 ;
/* grid search for parameters */
for ( t_steady_try = last_temp + 2.0 ; t_steady_try < = last_temp + 25.0 ; t_steady_try + = 0.5 ) {
for ( k_try = 0.00001 ; k_try < = 0.001 ; k_try * = 1.2 ) {
double error = 0.0 ;
int i ;
/* calculate error for this parameter set */
for ( i = 0 ; i < n_points ; i + + ) {
double t = points [ i ] . time ;
double t_predicted = t_steady_try - ( t_steady_try - points [ 0 ] . temperature ) * exp ( - k_try * t ) ;
double diff = points [ i ] . temperature - t_predicted ;
error + = diff * diff ;
}
2025-10-26 18:06:08 +01:00
2025-09-21 19:54:10 +02:00
if ( error < best_error ) {
best_error = error ;
best_k = k_try ;
best_t_steady = t_steady_try ;
}
}
}
model . k_heat = best_k ;
model . t_steady = best_t_steady ;
/* calculate quality metrics */
double sum_squared_residuals = 0.0 ;
double sum_total = 0.0 ;
double mean_temp = 0.0 ;
model . max_error = 0.0 ;
int i ;
/* calculate mean temperature */
for ( i = 0 ; i < n_points ; i + + )
mean_temp + = points [ i ] . temperature ;
mean_temp / = n_points ;
/* calculate R-squared and errors */
for ( i = 0 ; i < n_points ; i + + ) {
double t = points [ i ] . time ;
double t_predicted = model . t_steady - ( model . t_steady - points [ 0 ] . temperature ) * exp ( - model . k_heat * t ) ;
double residual = points [ i ] . temperature - t_predicted ;
2025-10-26 18:06:08 +01:00
2025-09-21 19:54:10 +02:00
sum_squared_residuals + = residual * residual ;
sum_total + = ( points [ i ] . temperature - mean_temp ) * ( points [ i ] . temperature - mean_temp ) ;
2025-10-26 18:06:08 +01:00
2025-09-21 19:54:10 +02:00
double abs_error = fabs ( residual ) ;
if ( abs_error > model . max_error ) {
model . max_error = abs_error ;
}
}
model . rmse = sqrt ( sum_squared_residuals / n_points ) ;
model . r_squared = 1.0 - ( sum_squared_residuals / sum_total ) ;
return model ;
}
void state_thermal ( struct snapraid_state * state , time_t now )
{
tommy_node * i ;
unsigned j ;
tommy_list high ;
tommy_list low ;
int ret ;
if ( state - > thermal_temperature_limit = = 0 )
return ;
tommy_list_init ( & high ) ;
tommy_list_init ( & low ) ;
/* for all disks */
for ( i = state - > disklist ; i ! = 0 ; i = i - > next ) {
struct snapraid_disk * disk = i - > data ;
devinfo_t * entry ;
entry = calloc_nofail ( 1 , sizeof ( devinfo_t ) ) ;
2026-04-28 00:35:15 +02:00
entry - > device = disk - > mount_device ;
2025-12-21 15:55:52 +01:00
device_name_set ( entry , disk - > name , 0 ) ;
2026-04-21 15:19:25 +02:00
pathcpy ( entry - > mount , sizeof ( entry - > mount ) , disk - > mount_point ) ;
2025-09-21 19:54:10 +02:00
pathcpy ( entry - > smartctl , sizeof ( entry - > smartctl ) , disk - > smartctl ) ;
memcpy ( entry - > smartignore , disk - > smartignore , sizeof ( entry - > smartignore ) ) ;
tommy_list_insert_tail ( & high , & entry - > node , entry ) ;
}
/* for all parities */
for ( j = 0 ; j < state - > level ; + + j ) {
devinfo_t * entry ;
unsigned s ;
for ( s = 0 ; s < state - > parity [ j ] . split_mac ; + + s ) {
entry = calloc_nofail ( 1 , sizeof ( devinfo_t ) ) ;
entry - > device = state - > parity [ j ] . split_map [ s ] . device ;
2025-12-21 15:55:52 +01:00
device_name_set ( entry , lev_config_name ( j ) , s ) ;
2025-09-21 19:54:10 +02:00
pathcpy ( entry - > mount , sizeof ( entry - > mount ) , state - > parity [ j ] . split_map [ s ] . path ) ;
pathcpy ( entry - > smartctl , sizeof ( entry - > smartctl ) , state - > parity [ j ] . smartctl ) ;
memcpy ( entry - > smartignore , state - > parity [ j ] . smartignore , sizeof ( entry - > smartignore ) ) ;
pathcut ( entry - > mount ) ; /* remove the parity file */
tommy_list_insert_tail ( & high , & entry - > node , entry ) ;
}
}
2026-02-08 17:47:58 +01:00
/* with a GUI always gives time reference */
if ( state - > opt . gui )
log_tag ( " unixtime:% " PRIi64 " \n " , ( int64_t ) now ) ;
2025-10-28 00:05:49 +01:00
if ( state - > opt . fake_device ) {
ret = devtest ( & high , & low , DEVICE_SMART ) ;
} else {
2026-03-11 13:53:42 +01:00
ret = devquery ( & high , & low , DEVICE_SMART ) ;
2025-10-28 00:05:49 +01:00
}
2025-09-21 19:54:10 +02:00
/* on error, just disable thermal gathering */
if ( ret ! = 0 )
return ;
/* if the list is empty, it's not supported in this platform */
if ( tommy_list_empty ( & low ) )
return ;
2026-02-08 18:24:48 +01:00
/* report new attribute */
state_attr ( state , & low ) ;
2025-09-21 19:54:10 +02:00
/* if ambient temperature is not set, set it now with the lowest HD temperature */
if ( state - > thermal_ambient_temperature = = 0 ) {
state - > thermal_ambient_temperature = ambient_temperature ( ) ;
for ( i = tommy_list_head ( & low ) ; i ! = 0 ; i = i - > next ) {
devinfo_t * devinfo = i - > data ;
int temp = smart_temp ( devinfo ) ;
if ( temp < 0 )
continue ;
2025-10-26 11:34:59 +01:00
log_tag ( " thermal:system:candidate:%d \n " , temp ) ;
2025-09-21 19:54:10 +02:00
if ( state - > thermal_ambient_temperature = = 0 | | state - > thermal_ambient_temperature > temp )
state - > thermal_ambient_temperature = temp ;
}
2025-10-26 11:34:59 +01:00
log_tag ( " thermal:system:final:%d \n " , state - > thermal_ambient_temperature ) ;
2025-09-21 19:54:10 +02:00
}
int highest_temperature = 0 ;
for ( i = tommy_list_head ( & low ) ; i ! = 0 ; i = i - > next ) {
tommy_node * t ;
struct snapraid_thermal * found ;
devinfo_t * devinfo = i - > data ;
unsigned k ;
int temperature = smart_temp ( devinfo ) ;
if ( temperature < 0 )
continue ;
/* search of the entry */
found = 0 ;
for ( t = tommy_list_head ( & state - > thermallist ) ; t ! = 0 ; t = t - > next ) {
struct snapraid_thermal * thermal = t - > data ;
if ( thermal - > device = = devinfo - > device ) {
found = thermal ;
break ;
}
}
/* if not found, create it */
if ( found = = 0 ) {
found = thermal_alloc ( devinfo - > device , devinfo - > name ) ;
tommy_list_insert_tail ( & state - > thermallist , & found - > node , found ) ;
}
found - > latest_temperature = temperature ;
2025-10-26 18:06:08 +01:00
2025-09-21 19:54:10 +02:00
if ( highest_temperature < temperature )
highest_temperature = temperature ;
log_tag ( " thermal:current:%s:% " PRIu64 " :%d \n " , devinfo - > name , devinfo - > device , temperature ) ;
if ( state - > thermal_stop_gathering )
continue ;
if ( found - > count + 1 > = THERMAL_MAX ) /* keep one extra space at the end */
continue ;
2025-10-17 09:12:21 +02:00
/* only monotone increasing temperature */
if ( found - > count > 0 & & found - > data [ found - > count - 1 ] . temperature > = temperature )
2025-09-21 19:54:10 +02:00
continue ;
/* insert the new data point */
found - > data [ found - > count ] . temperature = temperature ;
found - > data [ found - > count ] . time = now - state - > thermal_first ;
2025-10-17 09:12:21 +02:00
+ + found - > count ;
2025-10-16 17:55:22 +02:00
2025-10-28 00:05:49 +01:00
if ( state - > opt . fake_device ) {
/* fill with fake data */
found - > data [ 0 ] . time = 0 ;
found - > data [ 0 ] . temperature = 27 ;
found - > data [ 1 ] . time = 100 ;
found - > data [ 1 ] . temperature = 28 ;
found - > data [ 2 ] . time = 300 ;
found - > data [ 2 ] . temperature = 29 ;
found - > data [ 3 ] . time = 700 ;
found - > data [ 3 ] . temperature = 30 ;
found - > data [ 4 ] . time = 1500 ;
found - > data [ 4 ] . temperature = 31 ;
found - > count = 5 ;
}
2025-10-16 17:55:22 +02:00
/* log the new data */
2025-10-17 09:12:21 +02:00
log_tag ( " thermal:heat:%s:% " PRIu64 " :%u: " , devinfo - > name , devinfo - > device , found - > count ) ;
for ( k = 0 ; k < found - > count ; + + k )
2025-09-21 19:54:10 +02:00
log_tag ( " %s%d/%d " , k > 0 ? " , " : " " , ( int ) found - > data [ k ] . temperature , ( int ) found - > data [ k ] . time ) ;
log_tag ( " \n " ) ;
/* estimate parameters */
2025-10-17 09:12:21 +02:00
found - > params = fit_thermal_model ( found - > data , found - > count , state - > thermal_ambient_temperature ) ;
2025-09-21 19:54:10 +02:00
log_tag ( " thermal:params:%s:% " PRIu64 " :%g:%g:%g:%g:%g:%g \n " , devinfo - > name , devinfo - > device ,
found - > params . k_heat , found - > params . t_ambient , found - > params . t_steady ,
found - > params . rmse , found - > params . r_squared , found - > params . max_error ) ;
}
/* always update the highest temperature */
state - > thermal_highest_temperature = highest_temperature ;
log_tag ( " thermal:highest:%d \n " , highest_temperature ) ;
log_flush ( ) ;
tommy_list_foreach ( & high , free ) ;
tommy_list_foreach ( & low , free ) ;
}
int state_thermal_alarm ( struct snapraid_state * state )
{
/* if no limit, there is no thermal support */
if ( state - > thermal_temperature_limit = = 0 )
return 0 ;
if ( state - > thermal_highest_temperature < = state - > thermal_temperature_limit )
return 0 ;
return 1 ;
}
void state_thermal_cooldown ( struct snapraid_state * state )
{
2025-10-26 11:24:14 +01:00
int sleep_time = state - > thermal_cooldown_time ;
2025-10-26 18:06:08 +01:00
2025-09-21 19:54:10 +02:00
if ( sleep_time = = 0 )
2025-10-17 09:12:21 +02:00
sleep_time = 5 * 60 ; /* default sleep time */
2026-02-02 09:21:28 +01:00
if ( sleep_time < 3 * 60 )
sleep_time = 3 * 60 ; /* minimum sleep time */
2025-09-21 19:54:10 +02:00
/* from now on, stop any further data gathering as the heating is interrupted */
state - > thermal_stop_gathering = 1 ;
log_tag ( " thermal:spindown \n " ) ;
state_device ( state , DEVICE_DOWN , 0 ) ;
msg_progress ( " Cooldown... \n " ) ;
log_tag ( " thermal:cooldown:%d \n " , sleep_time ) ;
printf ( " Waiting for %d minutes... \n " , sleep_time / 60 ) ;
log_flush ( ) ;
2025-10-26 11:24:14 +01:00
/* every 30 seconds spin down any disk that was spunup */
while ( sleep_time > 0 ) {
state_device ( state , DEVICE_DOWNIFUP , 0 ) ;
sleep ( 30 ) ;
sleep_time - = 30 ;
}
2025-09-21 19:54:10 +02:00
2026-05-09 17:44:08 +02:00
if ( ! os_global_interrupt ( ) ) { /* don't wake-up if we are interrupting */
2025-09-21 19:54:10 +02:00
log_tag ( " thermal:spinup \n " ) ;
/* spinup */
state_device ( state , DEVICE_UP , 0 ) ;
/* log new thermal info */
state_thermal ( state , 0 ) ;
}
}
int state_thermal_begin ( struct snapraid_state * state , time_t now )
{
if ( state - > thermal_temperature_limit = = 0 )
return 1 ;
/* initial thermal measure */
state - > thermal_first = now ;
state - > thermal_latest = now ;
state_thermal ( state , now ) ;
if ( state - > thermal_ambient_temperature ! = 0 ) {
2025-10-26 11:34:59 +01:00
printf ( " System temperature is %u degrees \n " , state - > thermal_ambient_temperature ) ;
2025-09-21 19:54:10 +02:00
if ( state - > thermal_temperature_limit ! = 0 & & state - > thermal_temperature_limit < = state - > thermal_ambient_temperature ) {
/* LCOV_EXCL_START */
2026-02-01 20:21:28 +01:00
log_fatal ( EENVIRONMENT , " DANGER! System temperature of %d degrees is higher than the temperature limit of %d degrees. Unable to proceed! \n " , state - > thermal_ambient_temperature , state - > thermal_temperature_limit ) ;
2025-09-21 19:54:10 +02:00
log_flush ( ) ;
return 0 ;
/* LCOV_EXCL_STOP */
}
}
if ( state_thermal_alarm ( state ) ) {
/* LCOV_EXCL_START */
2026-02-01 20:21:28 +01:00
log_fatal ( EENVIRONMENT , " DANGER! Hard disk temperature of %d degrees is already outside the operating range. Unable to proceed! \n " , state - > thermal_highest_temperature ) ;
2025-09-21 19:54:10 +02:00
log_flush ( ) ;
return 0 ;
/* LCOV_EXCL_STOP */
}
2025-10-26 18:06:08 +01:00
2025-09-21 19:54:10 +02:00
return 1 ;
}
2025-10-26 18:06:08 +01:00