1313
1414// Include necessary headers
1515// cppcheck-suppress-begin missingIncludeSystem
16+ #include <ctype.h>
1617#include <curl/curl.h>
1718#include <jansson.h>
1819#include <stdio.h>
2930/** @brief Cached CURL handle for sensor polling. */
3031static CURL * sensor_curl_handle = NULL ;
3132
33+ static int contains_ci (const char * haystack , const char * needle )
34+ {
35+ if (!haystack || !needle || needle [0 ] == '\0' )
36+ return 0 ;
37+
38+ const size_t needle_len = strlen (needle );
39+ for (const char * p = haystack ; * p ; p ++ )
40+ {
41+ size_t i = 0 ;
42+ while (i < needle_len && p [i ] &&
43+ tolower ((unsigned char )p [i ]) ==
44+ tolower ((unsigned char )needle [i ]))
45+ {
46+ i ++ ;
47+ }
48+ if (i == needle_len )
49+ return 1 ;
50+ }
51+ return 0 ;
52+ }
53+
54+ static int is_cooling_duty_name (const char * name )
55+ {
56+ return contains_ci (name , "fan" ) || contains_ci (name , "pump" ) ||
57+ contains_ci (name , "cooler" ) || contains_ci (name , "cooling" );
58+ }
59+
60+ static int is_load_duty_name (const char * name )
61+ {
62+ return contains_ci (name , "load" ) || contains_ci (name , "usage" ) ||
63+ contains_ci (name , "util" ) || contains_ci (name , "activity" ) ||
64+ contains_ci (name , "process" ) || contains_ci (name , "processor" ) ||
65+ contains_ci (name , "gpu" ) || contains_ci (name , "cpu" );
66+ }
67+
3268/** @brief Init or return cached CURL handle. */
3369static CURL * get_sensor_curl_handle (void )
3470{
@@ -416,6 +452,41 @@ static const char *get_legacy_slot_device_type(const char *slot_value)
416452 return NULL ;
417453}
418454
455+ static const sensor_entry_t * find_preferred_legacy_duty_sensor (
456+ const monitor_sensor_data_t * data , const char * slot_value ,
457+ const char * target_type )
458+ {
459+ const sensor_entry_t * first_matching = NULL ;
460+ const sensor_entry_t * first_non_cooling = NULL ;
461+
462+ for (int i = 0 ; i < data -> sensor_count ; i ++ )
463+ {
464+ const sensor_entry_t * sensor = & data -> sensors [i ];
465+ if (sensor -> category != SENSOR_CATEGORY_DUTY ||
466+ strcmp (sensor -> device_type , target_type ) != 0 )
467+ continue ;
468+
469+ if (!first_matching )
470+ first_matching = sensor ;
471+
472+ if (!is_cooling_duty_name (sensor -> name ))
473+ {
474+ if (!first_non_cooling )
475+ first_non_cooling = sensor ;
476+ if (is_load_duty_name (sensor -> name ))
477+ return sensor ;
478+ }
479+ }
480+
481+ if (strcmp (slot_value , "gpu" ) == 0 )
482+ {
483+ /* Avoid rendering GPU fan duty as GPU processor utilisation. */
484+ return first_non_cooling ;
485+ }
486+
487+ return first_non_cooling ? first_non_cooling : first_matching ;
488+ }
489+
419490/**
420491 * @brief Resolve a legacy slot value to matching sensor entry.
421492 * @details Maps "cpu"→first CPU sensor, "gpu"→first GPU sensor,
@@ -432,6 +503,9 @@ static const sensor_entry_t *resolve_legacy_slot(
432503 if (!target_type )
433504 return NULL ;
434505
506+ if (category == SENSOR_CATEGORY_DUTY )
507+ return find_preferred_legacy_duty_sensor (data , slot_value , target_type );
508+
435509 for (int i = 0 ; i < data -> sensor_count ; i ++ )
436510 {
437511 if (data -> sensors [i ].category == category &&
0 commit comments