Skip to content

Fix crash on empty system stats aggregations#317

Open
jonathanendersby wants to merge 2 commits intotomaae:masterfrom
jonathanendersby:fix/empty-systemstats-crash
Open

Fix crash on empty system stats aggregations#317
jonathanendersby wants to merge 2 commits intotomaae:masterfrom
jonathanendersby:fix/empty-systemstats-crash

Conversation

@jonathanendersby
Copy link

@jonathanendersby jonathanendersby commented Mar 7, 2026

Summary

Fixes a crash in the coordinator when TrueNAS returns empty aggregation data from the reporting stats API. This commonly occurs after a TrueNAS reboot or when the reporting service hasn't collected enough data yet.

Three crash points fixed:

  1. ValueError: max() iterable argument is emptycputemp processing calls max() on mean.values() without checking if the dict is empty
  2. KeyError: 'cpu_cpu' — CPU usage reads self.ds["system_info"]["cpu_cpu"] directly, which doesn't exist when _systemstats_process failed to populate it
  3. KeyError: 'memory-free_value' — Memory usage calculation accesses memory-free_value directly, which doesn't exist when memory stats are empty
  4. KeyError in _systemstats_process — Direct dict key access graph["aggregations"]["mean"][e] fails when the key is missing from a sparse mean dict

Changes:

  • Check that mean dict is non-empty before calling max() on cputemp values
  • Use .get("cpu_cpu", 0.0) for safe CPU usage access
  • Use .get("memory-free_value", 0) for safe memory free value access
  • Use .get(e, 0.0) in _systemstats_process for safe mean value lookup

All fixes fall back to 0.0 defaults, so sensors show zero rather than crashing the entire integration.

Fixes #216, fixes #193, fixes #55

Test plan

  • Tested on TrueNAS SCALE with this integration — confirmed sensors recover after reboot
  • Verify sensors show 0 values briefly after TrueNAS reboot, then populate with real data once reporting service catches up

Summary by Sourcery

Handle empty or missing TrueNAS system stats aggregations without crashing the coordinator and default metrics to zero when data is unavailable.

Bug Fixes:

  • Prevent CPU temperature processing from crashing when aggregation means are missing or empty.
  • Avoid KeyError when reading CPU usage by safely accessing the computed cpu metric with a default value.
  • Avoid KeyError when computing memory usage by safely accessing memory-free values with a default fallback.
  • Prevent crashes in system stats processing when expected aggregation keys are absent by using safe mean lookups.

Guard against empty or missing `mean` dictionaries in the reporting
stats response, which can occur after a TrueNAS reboot or when the
reporting service hasn't collected data yet.

Fixes:
- `ValueError: max() iterable argument is empty` when cputemp
  aggregations mean dict is empty
- `KeyError: 'cpu_cpu'` when _systemstats_process fails to populate
  cpu stats due to missing keys in mean dict
- `KeyError: 'memory-free_value'` when memory stats are unavailable

Uses `.get()` with safe defaults and checks that the mean dict is
non-empty before calling `max()`.

Fixes tomaae#216, fixes tomaae#193, fixes tomaae#55
@sourcery-ai
Copy link

sourcery-ai bot commented Mar 7, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Handles empty or sparse system stats aggregation responses from TrueNAS without crashing by adding defensive checks and safe defaults when reading CPU temperature, CPU usage, and memory metrics from the reporting stats API.

Sequence diagram for updated system stats handling in get_systemstats

sequenceDiagram
    participant HA as HomeAssistant
    participant Coord as TrueNASCoordinator
    participant API as TrueNASReportingAPI

    HA->>Coord: get_systemstats()
    Coord->>API: fetch system stats
    API-->>Coord: tmp_graph (may have empty aggregations)

    loop Iterate_tmp_graph_entries
        Coord->>Coord: check entry name

        alt cputemp_entry
            Coord->>Coord: check aggregations and mean exist
            alt mean_present_and_non_empty
                Coord->>Coord: cpu_temperature = round(max(mean.values), 2)
            else mean_missing_or_empty
                Coord->>Coord: skip cpu_temperature update (no crash)
            end
        end

        alt cpu_entry
            Coord->>Coord: _systemstats_process(arr, graph, cpu)
            Coord->>Coord: cpu_usage = round(system_info.get(cpu_cpu, 0.0), 2)
        end

        alt memory_entry
            Coord->>Coord: _systemstats_process(arr, graph, memory)
            Coord->>Coord: check memory-total_value > 0
            alt total_value_positive
                Coord->>Coord: memory_free = float(system_info.get(memory-free_value, 0))
                Coord->>Coord: memory-usage_percent = round(100 * (total - memory_free) / total)
            else total_value_zero_or_missing
                Coord->>Coord: skip memory-usage_percent update (no crash)
            end
        end
    end

    Coord-->>HA: Updated ds with safe defaults (0.0) when data missing
Loading

Class diagram for updated coordinator system stats processing

classDiagram
    class TrueNASCoordinator {
        +dict ds
        +get_systemstats() void
        +_systemstats_process(arr, graph, t) void
    }

    class SystemInfoStore {
        +float cpu_temperature
        +float cpu_usage
        +float cpu_cpu
        +float memory_total_value
        +float memory_free_value
        +float memory_usage_percent
    }

    TrueNASCoordinator o-- SystemInfoStore : uses ds[system_info]

    class get_systemstats {
        +handles_cputemp_with_aggregation_check()
        +computes_cpu_usage_with_safe_default()
        +computes_memory_usage_with_safe_default()
    }

    class _systemstats_process {
        +reads_graph_aggregations_mean_safely()
        +sets_memory_free_value_for_available()
    }

    TrueNASCoordinator ..> get_systemstats
    TrueNASCoordinator ..> _systemstats_process

    class TmpGraphEntry {
        +string name
        +dict aggregations
    }

    class Aggregations {
        +dict mean
    }

    TmpGraphEntry o-- Aggregations

    get_systemstats ..> TmpGraphEntry
    _systemstats_process ..> Aggregations

    note for get_systemstats "CPU temperature: only uses max(mean.values) when aggregations.mean exists and is non-empty"
    note for get_systemstats "CPU usage: uses system_info.get(cpu_cpu, 0.0) to avoid KeyError"
    note for get_systemstats "Memory usage: uses system_info.get(memory-free_value, 0) before percentage calculation"
    note for _systemstats_process "Mean lookup uses graph[aggregations][mean].get(e, 0.0) to avoid KeyError"
Loading

File-Level Changes

Change Details Files
Guard CPU temperature aggregation against empty or missing mean data to avoid max() on an empty iterable.
  • Add an additional condition to the cputemp branch to ensure an aggregations.mean mapping exists before computing max()
  • Leave existing rounding logic for cpu_temperature unchanged when valid data is present
custom_components/truenas/coordinator.py
Make CPU usage calculation resilient to missing cpu_cpu values in the system_info data store.
  • Change direct indexing of the cpu_cpu key in system_info to a .get() call with a 0.0 default before rounding
  • Preserve existing call to _systemstats_process for CPU graphs
custom_components/truenas/coordinator.py
Make memory usage percentage computation robust to missing memory-free_value and sparse mean aggregations.
  • Introduce a local memory_free variable that reads memory-free_value from system_info via .get() with a 0 default
  • Use memory_free in the memory-usage_percent calculation instead of directly indexing memory-free_value
  • Update _systemstats_process to read mean values from graph.aggregations.mean via .get(e, 0.0) to avoid KeyError on sparse aggregation data
custom_components/truenas/coordinator.py

Assessment against linked issues

Issue Objective Addressed Explanation
#55 Prevent the TrueNAS integration from crashing during setup when system stats aggregations are empty, specifically avoiding ValueError: max() arg is an empty sequence in get_systemstats.
#193 Prevent the TrueNAS Home Assistant integration from crashing in get_systemstats when TrueNAS returns empty or missing aggregation data (specifically the ValueError from max() on an empty mean dict for CPU temperature).
#216 Prevent the TrueNAS integration from crashing during setup when the reporting stats API returns empty aggregation data (e.g., for cputemp), so that Home Assistant no longer logs ValueError: max() iterable argument is empty and the integration can initialize successfully.
#216 Ensure the coordinator handles missing or sparse system stats data (CPU usage, memory stats) without raising KeyError or other exceptions, allowing entities to be created and the integration to continue updating even when TrueNAS has not yet populated reporting data.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In get_systemstats you now guard against missing aggregations.mean for cputemp, but still index tmp_graph[i]["aggregations"]["mean"] directly; consider assigning mean = tmp_graph[i]["aggregations"].get("mean") once and reusing it to avoid repeat lookups and potential inconsistencies.
  • In _systemstats_process, graph["aggregations"]["mean"] is still accessed via direct indexing; if mean can be absent in the same scenarios where individual keys are missing, consider using .get("aggregations", {}).get("mean", {}) (or an early return) to avoid additional KeyErrors.
  • For consistency with the new guarded access patterns, you may want to revisit other self.ds["system_info"][...] lookups in get_systemstats (e.g., memory-total_value) and either enforce their presence explicitly or use .get(...) where they may be legitimately absent after a reboot.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `get_systemstats` you now guard against missing `aggregations.mean` for `cputemp`, but still index `tmp_graph[i]["aggregations"]["mean"]` directly; consider assigning `mean = tmp_graph[i]["aggregations"].get("mean")` once and reusing it to avoid repeat lookups and potential inconsistencies.
- In `_systemstats_process`, `graph["aggregations"]["mean"]` is still accessed via direct indexing; if `mean` can be absent in the same scenarios where individual keys are missing, consider using `.get("aggregations", {}).get("mean", {})` (or an early return) to avoid additional `KeyError`s.
- For consistency with the new guarded access patterns, you may want to revisit other `self.ds["system_info"][...]` lookups in `get_systemstats` (e.g., `memory-total_value`) and either enforce their presence explicitly or use `.get(...)` where they may be legitimately absent after a reboot.

## Individual Comments

### Comment 1
<location path="custom_components/truenas/coordinator.py" line_range="474-477" />
<code_context>
                     e = tmp_var

-                    tmp_val = graph["aggregations"]["mean"][e] or 0.0
+                    tmp_val = graph["aggregations"]["mean"].get(e, 0.0) or 0.0
                     if t == "memory":
                         if tmp_var == "available":
</code_context>
<issue_to_address>
**suggestion:** Redundant `or 0.0` after `.get(e, 0.0)`

With `.get(e, 0.0)`, the trailing `or 0.0` only changes behavior for falsy non-`None` values like `0` or `""`, which is undesirable since `0` is a valid metric. Either use `graph["aggregations"]["mean"].get(e) or 0.0` (to only guard against `None`) or keep `.get(e, 0.0)` alone and drop `or 0.0`.

```suggestion
                    e = tmp_var

                    tmp_val = graph["aggregations"]["mean"].get(e, 0.0)
                    if t == "memory":
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Keeps the `or 0.0` to guard against None values from the API while
avoiding the subtle issue of coercing a legitimate 0 metric value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

[Bug] Failed setup, will retry: max() iterable argument is empty [Bug] - Unexpected error fetching TrueNAS data [Bug] max fails with empty list

1 participant