Skip to content

Conversation

@MitchellThompkins
Copy link

@MitchellThompkins MitchellThompkins commented Feb 1, 2026

Temperature Monitoring System Feature Request

Description

This PR implements a comprehensive temperature monitoring system integrated into the existing MetricsResolver. It provides real-time access to temperature data from various system sensors via GraphQL queries and subscriptions.

I tested these changes locally using the provided Apollo Server on Unraid 7.2.0.

Addresses #1597

Key Features

  • Multi-Source Support:
    • CPU & Motherboard: Via lm-sensors integration.
    • Disks: Modifies smartctl integration
      • Modifies smartctl to leverage json parsing instead of raw strings.
    • IPMI: Support for ipmitool sensors (see notes below!).
  • GraphQL API:
    • New temperature field on the Metrics node.
    • systemMetricsTemperature subscription for real-time updates.
    • Exposes current, min, max, and history for each sensor.
  • Configuration:
    • Fully configurable via api.json (enabled status, polling intervals, thresholds).
    • Support for default_unit preference (Celsius/Fahrenheit) with automatic conversion.
  • History & Aggregation:
    • In-memory history tracking with configurable retention.
    • Summary statistics (average temp, hottest/coolest sensor counts).

Implementation Details

  • Binary Management: Relies on system-installed tools (sensors, smartctl, ipmitool) rather than bundling binaries, aligning with the base OS integration strategy. There is no attempt here to package sensor tooling as part of this feature. This is per a conversation on the feature scope with the Unraid team.
  • Architecture: Implemented a modular TemperatureSensorProvider interface allowing for easy addition of new sensor types.
  • Robustness: DisksService was updated to parse smartctl JSON output directly, resolving issues with raw string parsing on certain drive models. There was an issue with some Seagate drives reporting raw values with extra data in parentheses: 24 (0 14 0 0 0). Parsing the last value in the string returned incorrect data. Parsing with json prevents this.
  • Testing: Added unit tests for all additional modules.

Documentation

  • Added developer documentation at docs/developer/temperature.md detailing configuration options and API usage.

Scope Notes

  • IPMI Integration: The IpmiSensorsService has been implemented to parse standard ipmitool output, but it has not been tested on live hardware as I do not have access to a system with IPMI support. It relies on standard ipmitool output formats (at least the documentation I saw and what AI told me).
  • GPU: GPU temperature monitoring is currently out of scope and has not been implemented. I do not have access to a machine with a GPU and could not reliably test it.
  • Alerts: The API calculates and exposes WARNING and CRITICAL statuses based on thresholds but does not currently trigger active system notifications. This type of alert is passive only.

Summary by CodeRabbit

  • New Features

    • Temperature monitoring: configurable polling (default 5000ms), default unit (Celsius), history/retention, thresholds, multiple sensor providers (lm-sensors, SMART, IPMI), per-sensor current/min/max/history, summaries, and live metrics subscription; metrics now include temperature.
  • Documentation

    • Developer docs added with configuration examples, sensor options, and GraphQL query/subscription details.
  • Tests

    • Extensive unit and integration tests for providers, parsing, history retention, thresholds, summaries, and error scenarios.

@codecov
Copy link

codecov bot commented Feb 1, 2026

Codecov Report

❌ Patch coverage is 77.89474% with 168 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.80%. Comparing base (bb9b539) to head (f735e9b).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
...solvers/metrics/temperature/temperature.service.ts 78.75% 65 Missing ⚠️
...etrics/temperature/sensors/ipmi_sensors.service.ts 28.78% 47 Missing ⚠️
...resolvers/metrics/temperature/temperature.model.ts 62.31% 26 Missing ⚠️
...metrics/temperature/temperature_history.service.ts 89.43% 15 Missing ⚠️
...id-api/graph/resolvers/metrics/metrics.resolver.ts 61.29% 12 Missing ⚠️
...nraid-api/graph/resolvers/metrics/metrics.model.ts 40.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1876      +/-   ##
==========================================
+ Coverage   46.39%   46.80%   +0.40%     
==========================================
  Files         954      962       +8     
  Lines       59770    60554     +784     
  Branches     5537     5705     +168     
==========================================
+ Hits        27730    28341     +611     
- Misses      31921    32094     +173     
  Partials      119      119              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@api/src/unraid-api/graph/resolvers/metrics/temperature/temperature.service.ts`:
- Around line 208-259: After mapping/filtering sensors, guard the fast-path
against empty sensor sets by checking sensors.length and returning early with
the same response shape but summary set to null (e.g. id 'temperature-metrics',
sensors: [], summary: null) instead of calling buildSummary; this prevents
buildSummary from throwing on an empty array. Apply the same early-return guard
wherever the same fast-path occurs (see the other occurrence around the
buildSummary call at the later block referenced by lines 369-372). Ensure you
reference the sensors variable and buildSummary function when adding the check.
- Around line 129-133: Replace the unsafe enum lookup and any-typed threshold
params by adding a typed parser and a TemperatureThresholdConfig type, then use
them: implement a private parseUnit(configUnit: string): TemperatureUnit that
converts configUnit.toUpperCase() safely to a TemperatureUnit (falling back to
TemperatureUnit.CELSIUS) and replace occurrences of (TemperatureUnit as
any)[configUnit.toUpperCase()] with this.parseUnit(configUnit); define an
interface/type TemperatureThresholdConfig for the thresholds shape and change
the methods that accept thresholdConfig/thresholds to use that type (e.g.,
update the parameter signatures referenced as thresholdConfig and thresholds in
the class to TemperatureThresholdConfig).
🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/metrics/temperature/temperature.service.ts (2)

21-21: Remove the redundant file header comment.

It doesn’t add clarity and conflicts with the no-unnecessary-comments guideline.

♻️ Proposed change
-// temperature.service.ts

As per coding guidelines, "Never add comments unless they are needed for clarity of function".


154-154: Use function overloads to avoid type assertions on convertReading calls.

Remove the as TemperatureReading casts at lines 154 and 218 by adding overloads to the convertReading method. This eliminates unnecessary type assertions while maintaining full type safety:

♻️ Suggested overloads
-private convertReading(
-    reading: TemperatureReading | undefined,
-    targetUnit: TemperatureUnit
-): TemperatureReading | undefined {
+private convertReading(
+    reading: TemperatureReading,
+    targetUnit: TemperatureUnit
+): TemperatureReading;
+private convertReading(
+    reading: TemperatureReading | undefined,
+    targetUnit: TemperatureUnit
+): TemperatureReading | undefined;
+private convertReading(
+    reading: TemperatureReading | undefined,
+    targetUnit: TemperatureUnit
+): TemperatureReading | undefined {
     if (!reading) return undefined;
 
     return {
         ...reading,
         value: this.convertValue(reading.value, reading.unit, targetUnit),
         unit: targetUnit,
     };
 }
-const current = this.convertReading(rawCurrent, targetUnit) as TemperatureReading;
+const current = this.convertReading(rawCurrent, targetUnit);
-const current = this.convertReading(rawCurrent, targetUnit) as TemperatureReading;
+const current = this.convertReading(rawCurrent, targetUnit);

Also applies to line 218.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@api/src/unraid-api/graph/resolvers/metrics/temperature/temperature.service.spec.ts`:
- Around line 25-53: The test currently uses unsafe as any casts for mocks
(lmSensors, diskSensors, ipmiSensors, configService); replace those with
properly typed mocks by declaring the sensor mocks as
Partial<TemperatureSensorProvider> objects implementing the needed methods
(isAvailable, read) and by instantiating a real ConfigService and using
vi.spyOn(configService, 'get') to control return values instead of casting to
any—update references in the spec to use the typed
Partial<TemperatureSensorProvider> mocks and the spied ConfigService so the
compiler enforces correct method signatures while keeping the same mock
behavior.
🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/metrics/temperature/temperature.service.ts (1)

21-38: Trim redundant inline comments.
The file/constructor comments restate the code intent; removing them keeps the file aligned with the no-comment guideline.

As per coding guidelines: **/*.{ts,tsx,js,jsx}: Never add comments unless they are needed for clarity of function.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@api/docs/developer/temperature.md`:
- Line 18: The table entry for history.max_readings currently lists a default of
1000 but the sample config uses 144; fix the inconsistency by either updating
the table default for history.max_readings to 144 to match the sample, or keep
the table at 1000 and add a clear inline comment next to the sample
configuration showing that the sample intentionally overrides the default (e.g.,
"override for low-memory devices: 144") so readers understand the deviation;
locate references to history.max_readings in the docs and make the change in the
table row and/or the sample block accordingly.
🧹 Nitpick comments (1)
api/docs/developer/temperature.md (1)

18-19: Clarify or remove "(Internal)" designation.

Both history.max_readings and history.retention_ms are marked as "(Internal)" in the description, yet they appear as user-configurable settings in the sample configuration (lines 60-62). This is confusing.

If these settings are configurable by users, remove the "(Internal)" label. If they're meant to be internal-only, explain why they're documented in user-facing configuration documentation.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@api/src/unraid-api/graph/resolvers/metrics/temperature/temperature.service.ts`:
- Line 238: Replace the non-null assertion on metadata from
this.history.getMetadata(sensorId)! with a defensive check: store the result in
a const (e.g., const metadata = this.history.getMetadata(sensorId)); if metadata
is null/undefined, handle it explicitly (return early, throw a clear error, or
fallback behavior) before using metadata; ensure references to metadata in the
surrounding method (the variable named metadata and the sensorId parameter) are
guarded so no runtime exception can occur if history was cleared.
🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/metrics/temperature/temperature.service.ts (1)

30-30: Remove unnecessary file name comment.

This comment adds no value since the file name is already visible in the file path.

🧹 Suggested fix
-// temperature.service.ts
 `@Injectable`()
 export class TemperatureService implements OnModuleInit {

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts (1)

412-417: ⚠️ Potential issue | 🟡 Minor

Inconsistent use of any type.

Line 308 and 388 were correctly updated to use unknown, but this occurrence was missed.

Suggested fix
-            vi.mocked(configService.get).mockImplementation((key: string, defaultValue?: any) => {
+            vi.mocked(configService.get).mockImplementation((key: string, defaultValue?: unknown) => {

As per coding guidelines: "Never use the any type. Always prefer proper typing."

🤖 Fix all issues with AI agents
In
`@api/src/unraid-api/graph/resolvers/metrics/temperature/temperature.service.spec.ts`:
- Around line 430-447: Remove the stale inline comment ("// Document expected
behavior...") in the test and update the assertion to match the current behavior
where non-finite values are filtered: after calling service.onModuleInit(),
mocking lmSensors.read() to return the NaN sensor, assert via
service.getMetrics() that the returned metrics do not include the 'nan-sensor'
entry (or assert the returned metrics array is empty) instead of leaving the
misleading comment or expecting metrics toBeNull; locate this change around the
test that mocks lmSensors.read!, the onModuleInit() call, and the getMetrics()
assertion.
🧹 Nitpick comments (5)
api/src/unraid-api/graph/resolvers/disks/disks.service.spec.ts (1)

24-25: Remove unnecessary comment.

This empty comment section adds no value and violates the coding guideline about avoiding unnecessary comments.

Suggested fix
-// Vitest imports
-
 // Mock the external dependencies using vi

As per coding guidelines: "Never add comments unless they are needed for clarity of function."

api/src/unraid-api/graph/resolvers/metrics/temperature/sensors/disk_sensors.service.spec.ts (2)

37-75: Avoid as unknown as Disk fixtures.
The double-casts bypass type safety. Prefer a typed disk fixture factory (or satisfies Disk) so tests stay aligned with the model.

As per coding guidelines: Avoid using casting whenever possible, prefer proper typing from the start.


141-170: Align test name with the interface type under test.
The “nvme interface” case currently uses DiskInterfaceType.PCIE. If an NVME enum exists, add an explicit case; otherwise rename the test to PCIE for clarity.

api/src/unraid-api/graph/resolvers/metrics/temperature/temperature.service.ts (2)

30-63: Trim redundant comments.
The file/section headers restate the code and aren’t adding clarity; removing them keeps the file cleaner.

🧹 Suggested cleanup
-// temperature.service.ts
 `@Injectable`()
 export class TemperatureService implements OnModuleInit {
@@
-        // Inject all available sensor providers
         private readonly lmSensors: LmSensorsService,
         private readonly diskSensors: DiskSensorsService,
         private readonly ipmiSensors: IpmiSensorsService,
@@
-        // Initialize all providers and check availability
         await this.initializeProviders();
As per coding guidelines: Never add comments unless they are needed for clarity of function.

173-176: Avoid as TemperatureReading casts via overloads.
You can overload convertReading so callers with defined readings get a non-optional return.

♻️ Suggested refactor
-                const current = this.convertReading(rawCurrent, targetUnit) as TemperatureReading;
+                const current = this.convertReading(rawCurrent, targetUnit);
@@
-                const current = this.convertReading(rawCurrent, targetUnit) as TemperatureReading;
+                const current = this.convertReading(rawCurrent, targetUnit);
@@
-    private convertReading(
-        reading: TemperatureReading | undefined,
-        targetUnit: TemperatureUnit
-    ): TemperatureReading | undefined {
+    private convertReading(
+        reading: TemperatureReading,
+        targetUnit: TemperatureUnit
+    ): TemperatureReading;
+    private convertReading(
+        reading: TemperatureReading | undefined,
+        targetUnit: TemperatureUnit
+    ): TemperatureReading | undefined {
         if (!reading) return undefined;
 
         return {
             ...reading,
             value: this.convertValue(reading.value, reading.unit, targetUnit),
             unit: targetUnit,
         };
     }
As per coding guidelines: Avoid using casting whenever possible, prefer proper typing from the start.

Also applies to: 242-244, 287-298

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant