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
@@ -25,6 +25,7 @@ from wled_controller.api.schemas.value_sources import (
HAEntityValueSourceResponse,
StaticColorValueSourceResponse,
StaticValueSourceResponse,
SystemMetricsValueSourceResponse,
ValueSourceCreate,
ValueSourceListResponse,
ValueSourceResponse,
@@ -42,6 +43,7 @@ from wled_controller.storage.value_source import (
HAEntityValueSource,
StaticColorValueSource,
StaticValueSource,
SystemMetricsValueSource,
ValueSource,
)
from wled_controller.storage.value_source_store import ValueSourceStore
@@ -171,6 +173,22 @@ _RESPONSE_MAP = {
led_start=s.led_start,
led_end=s.led_end,
),
SystemMetricsValueSource: lambda s: SystemMetricsValueSourceResponse(
id=s.id,
name=s.name,
description=s.description,
tags=s.tags,
created_at=s.created_at,
updated_at=s.updated_at,
metric=s.metric,
min_value=s.min_value,
max_value=s.max_value,
max_rate=s.max_rate,
disk_path=s.disk_path,
sensor_label=s.sensor_label,
poll_interval=s.poll_interval,
smoothing=s.smoothing,
),
}
@@ -403,7 +421,8 @@ async def test_value_source_ws(
raw = stream.get_raw_value()
if raw is not None:
msg["raw_value"] = round(raw, 4)
msg["raw_range"] = [stream._min_ha, stream._max_ha]
if hasattr(stream, "_min_ha"):
msg["raw_range"] = [stream._min_ha, stream._max_ha]
await websocket.send_json(msg)
await asyncio.sleep(0.05)
except WebSocketDisconnect:
@@ -124,6 +124,19 @@ class CSSExtractValueSourceResponse(_ValueSourceResponseBase):
led_end: int = Field(description="End of LED range (-1 = whole strip)")
class SystemMetricsValueSourceResponse(_ValueSourceResponseBase):
source_type: Literal["system_metrics"] = "system_metrics"
return_type: Literal["float"] = "float"
metric: str = Field(description="System metric to monitor")
min_value: float = Field(description="Min value for range-based metrics")
max_value: float = Field(description="Max value for range-based metrics")
max_rate: float = Field(description="Max rate in bytes/sec for network metrics")
disk_path: str = Field(description="Disk path for disk_usage metric")
sensor_label: str = Field(description="Sensor label for cpu_temp/fan_speed")
poll_interval: float = Field(description="Seconds between reads")
smoothing: float = Field(description="EMA smoothing factor (0.0-1.0)")
ValueSourceResponse = Annotated[
Union[
Annotated[StaticValueSourceResponse, Tag("static")],
@@ -138,6 +151,7 @@ ValueSourceResponse = Annotated[
Annotated[HAEntityValueSourceResponse, Tag("ha_entity")],
Annotated[GradientMapValueSourceResponse, Tag("gradient_map")],
Annotated[CSSExtractValueSourceResponse, Tag("css_extract")],
Annotated[SystemMetricsValueSourceResponse, Tag("system_metrics")],
],
Discriminator("source_type"),
]
@@ -252,6 +266,18 @@ class CSSExtractValueSourceCreate(_ValueSourceCreateBase):
led_end: int = Field(-1, description="End of LED range (-1 = whole strip)")
class SystemMetricsValueSourceCreate(_ValueSourceCreateBase):
source_type: Literal["system_metrics"] = "system_metrics"
metric: str = Field("cpu_load", description="System metric to monitor")
min_value: float = Field(0.0, description="Min value for normalization")
max_value: float = Field(100.0, description="Max value for normalization")
max_rate: float = Field(125_000_000.0, description="Max rate bytes/sec for network")
disk_path: str = Field("", description="Disk path for disk_usage")
sensor_label: str = Field("", description="Sensor label for cpu_temp/fan_speed")
poll_interval: float = Field(1.0, description="Poll interval in seconds", ge=0.1, le=60.0)
smoothing: float = Field(0.0, description="EMA smoothing (0.0-1.0)", ge=0.0, le=1.0)
ValueSourceCreate = Annotated[
Union[
Annotated[StaticValueSourceCreate, Tag("static")],
@@ -266,6 +292,7 @@ ValueSourceCreate = Annotated[
Annotated[HAEntityValueSourceCreate, Tag("ha_entity")],
Annotated[GradientMapValueSourceCreate, Tag("gradient_map")],
Annotated[CSSExtractValueSourceCreate, Tag("css_extract")],
Annotated[SystemMetricsValueSourceCreate, Tag("system_metrics")],
],
Discriminator("source_type"),
]
@@ -374,6 +401,18 @@ class CSSExtractValueSourceUpdate(_ValueSourceUpdateBase):
led_end: Optional[int] = Field(None, description="LED range end")
class SystemMetricsValueSourceUpdate(_ValueSourceUpdateBase):
source_type: Literal["system_metrics"] = "system_metrics"
metric: Optional[str] = Field(None, description="System metric")
min_value: Optional[float] = Field(None, description="Min value")
max_value: Optional[float] = Field(None, description="Max value")
max_rate: Optional[float] = Field(None, description="Max rate bytes/sec")
disk_path: Optional[str] = Field(None, description="Disk path")
sensor_label: Optional[str] = Field(None, description="Sensor label")
poll_interval: Optional[float] = Field(None, description="Poll interval", ge=0.1, le=60.0)
smoothing: Optional[float] = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
ValueSourceUpdate = Annotated[
Union[
Annotated[StaticValueSourceUpdate, Tag("static")],
@@ -388,6 +427,7 @@ ValueSourceUpdate = Annotated[
Annotated[HAEntityValueSourceUpdate, Tag("ha_entity")],
Annotated[GradientMapValueSourceUpdate, Tag("gradient_map")],
Annotated[CSSExtractValueSourceUpdate, Tag("css_extract")],
Annotated[SystemMetricsValueSourceUpdate, Tag("system_metrics")],
],
Discriminator("source_type"),
]