feat: system_metrics value source type
Lint & Test / test (push) Successful in 1m32s

New value source that monitors host hardware via psutil/pynvml:
cpu_load, cpu_temp, gpu_load, gpu_temp, ram_usage, disk_usage,
network_rx, network_tx, battery_level, fan_speed.

Each metric normalizes to 0.0-1.0 with configurable ranges, poll
interval, EMA smoothing, and sensor_label for multi-sensor systems.
Conditional editor fields show/hide based on selected metric.

Also fixes: WS test crash when raw_value streams lack _min_ha attr,
toast timer overlap on rapid calls, SW cache bump to v34.
This commit is contained in:
2026-03-30 18:22:58 +03:00
parent db5008aaeb
commit b6713be390
12 changed files with 618 additions and 10 deletions
@@ -42,6 +42,7 @@
<option value="ha_entity" data-i18n="value_source.type.ha_entity">HA Entity</option>
<option value="gradient_map" data-i18n="value_source.type.gradient_map">Gradient Map</option>
<option value="css_extract" data-i18n="value_source.type.css_extract">Strip Extract</option>
<option value="system_metrics" data-i18n="value_source.type.system_metrics">System Metrics</option>
</select>
</div>
@@ -455,6 +456,85 @@
</div>
</div>
<!-- System Metrics fields -->
<div id="value-source-system-metrics-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="value-source-metric" data-i18n="value_source.metric">Metric:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.metric.hint">System metric to monitor. Some metrics may be unavailable depending on hardware.</small>
<select id="value-source-metric">
<option value="cpu_load">CPU Load</option>
<option value="cpu_temp">CPU Temperature</option>
<option value="gpu_load">GPU Load</option>
<option value="gpu_temp">GPU Temperature</option>
<option value="ram_usage">RAM Usage</option>
<option value="disk_usage">Disk Usage</option>
<option value="network_rx">Network RX</option>
<option value="network_tx">Network TX</option>
<option value="battery_level">Battery Level</option>
<option value="fan_speed">Fan Speed</option>
</select>
</div>
<div id="value-source-sysmetric-range" style="display:none">
<div class="form-group">
<label for="value-source-sysmetric-min"><span data-i18n="value_source.sysmetric.min">Min Value:</span></label>
<input type="number" id="value-source-sysmetric-min" step="any" value="0">
</div>
<div class="form-group">
<label for="value-source-sysmetric-max"><span data-i18n="value_source.sysmetric.max">Max Value:</span></label>
<input type="number" id="value-source-sysmetric-max" step="any" value="100">
</div>
</div>
<div id="value-source-sysmetric-network" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="value-source-max-rate" data-i18n="value_source.max_rate">Max Rate (bytes/sec):</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.max_rate.hint">Maximum expected network rate in bytes/sec. 125000000 = 1 Gbps.</small>
<input type="number" id="value-source-max-rate" min="1" step="1" value="125000000">
</div>
</div>
<div id="value-source-sysmetric-disk" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="value-source-disk-path" data-i18n="value_source.disk_path">Disk Path:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.disk_path.hint">Disk mount point or drive letter (e.g. / or C:\)</small>
<input type="text" id="value-source-disk-path" placeholder="/" value="">
</div>
</div>
<div id="value-source-sysmetric-sensor" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="value-source-sensor-label" data-i18n="value_source.sensor_label">Sensor Label:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="value_source.sensor_label.hint">Optional sensor name to pick a specific sensor (empty = first available)</small>
<input type="text" id="value-source-sensor-label" placeholder="" value="">
</div>
</div>
<div class="form-group">
<label for="value-source-poll-interval"><span data-i18n="value_source.poll_interval">Poll Interval:</span> <span id="value-source-poll-interval-display">1.0</span>s</label>
<input type="range" id="value-source-poll-interval" min="0.5" max="10" step="0.5" value="1"
oninput="document.getElementById('value-source-poll-interval-display').textContent = this.value">
</div>
<div class="form-group">
<label for="value-source-sysmetric-smoothing"><span data-i18n="value_source.smoothing">Smoothing:</span> <span id="value-source-sysmetric-smoothing-display">0</span></label>
<input type="range" id="value-source-sysmetric-smoothing" min="0" max="0.99" step="0.01" value="0"
oninput="document.getElementById('value-source-sysmetric-smoothing-display').textContent = this.value">
</div>
</div>
<!-- Shared adaptive output range (shown for adaptive and daylight types) -->
<div id="value-source-adaptive-range-section" style="display:none">
<div class="form-group">