feat: server telemetry, update entity, sync-clock controls

- Server device exposing CPU/RAM/GPU/temperature/battery sensors via
  /api/v1/system/performance, plus last-restart timestamp (cached with
  jitter threshold so the recorder doesn't see poll wobble) and version.
- Update entity backed by /api/v1/system/update — installs via
  /apply, hides the install button when the server reports
  can_auto_update=false.
- Sync-clock entities: reset button, speed number, running switch, and
  the event listener now refreshes on entity_changed events too.
- Bump manifest to 0.4.0.
This commit is contained in:
2026-04-27 01:35:42 +03:00
parent e8f2b5e528
commit a666d9eb9c
12 changed files with 1080 additions and 23 deletions
+49 -6
View File
@@ -35,6 +35,7 @@ PLATFORMS: list[Platform] = [
Platform.SENSOR,
Platform.NUMBER,
Platform.SELECT,
Platform.UPDATE,
]
@@ -61,6 +62,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Create device entries for each target and remove stale ones
device_registry = dr.async_get(hass)
current_identifiers: set[tuple[str, str]] = set()
# Server device — owns the system telemetry sensors (CPU, RAM, GPU, etc.)
server_identifier = (DOMAIN, f"{entry.entry_id}_server")
sw_version = (
coordinator.server_version
if coordinator.server_version and coordinator.server_version != "unknown"
else None
)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={server_identifier},
name=server_name,
manufacturer="LedGrab",
model="Server",
sw_version=sw_version,
configuration_url=server_url,
)
current_identifiers.add(server_identifier)
if coordinator.data and "targets" in coordinator.data:
for target_id, target_data in coordinator.data["targets"].items():
info = target_data["info"]
@@ -93,6 +113,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
current_identifiers.add(scenes_identifier)
# One device per sync clock — groups its switch/number/button/sensor.
sync_clocks = coordinator.data.get("sync_clocks", []) if coordinator.data else []
for clock in sync_clocks:
clock_identifier = (DOMAIN, clock["id"])
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={clock_identifier},
name=clock.get("name", clock["id"]),
manufacturer=server_name,
model="Sync Clock",
configuration_url=server_url,
)
current_identifiers.add(clock_identifier)
# Remove devices for targets that no longer exist
for device_entry in dr.async_entries_for_config_entry(device_registry, entry.entry_id):
if not device_entry.identifiers & current_identifiers:
@@ -106,28 +140,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
DATA_EVENT_LISTENER: event_listener,
}
# Track target and scene IDs to detect changes
# Track target, scene, and sync-clock IDs to detect changes
known_target_ids = set(coordinator.data.get("targets", {}).keys() if coordinator.data else [])
known_scene_ids = set(
p["id"] for p in (coordinator.data.get("scene_presets", []) if coordinator.data else [])
)
known_clock_ids = set(
c["id"] for c in (coordinator.data.get("sync_clocks", []) if coordinator.data else [])
)
def _on_coordinator_update() -> None:
"""Detect target/scene list changes and trigger reload."""
nonlocal known_target_ids, known_scene_ids
"""Detect target/scene/clock list changes and trigger reload."""
nonlocal known_target_ids, known_scene_ids, known_clock_ids
if not coordinator.data:
return
targets = coordinator.data.get("targets", {})
# Reload if target or scene list changed
# Reload if target, scene, or sync-clock list changed
current_ids = set(targets.keys())
current_scene_ids = set(p["id"] for p in coordinator.data.get("scene_presets", []))
if current_ids != known_target_ids or current_scene_ids != known_scene_ids:
current_clock_ids = set(c["id"] for c in coordinator.data.get("sync_clocks", []))
if (
current_ids != known_target_ids
or current_scene_ids != known_scene_ids
or current_clock_ids != known_clock_ids
):
known_target_ids = current_ids
known_scene_ids = current_scene_ids
_LOGGER.info("Target or scene list changed, reloading integration")
known_clock_ids = current_clock_ids
_LOGGER.info("Target, scene, or sync-clock list changed, reloading integration")
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
coordinator.async_add_listener(_on_coordinator_update)