114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
import streamDeck, {
|
|
action,
|
|
type DidReceiveSettingsEvent,
|
|
type KeyAction,
|
|
type KeyDownEvent,
|
|
SingletonAction,
|
|
type WillAppearEvent,
|
|
type WillDisappearEvent,
|
|
} from "@elgato/streamdeck";
|
|
import { type GpuMetricKind } from "../lib/metrics-parser";
|
|
import { combineSeries } from "../lib/metrics-poller";
|
|
import { renderGpuGraph, svgDataUrl } from "../lib/render";
|
|
import { runtime } from "../lib/runtime";
|
|
import { cfgFromSettings, type CfgSettings } from "../lib/util";
|
|
|
|
type GpuSettings = CfgSettings & {
|
|
gpuId?: string;
|
|
metric?: GpuMetricKind;
|
|
gpuCombo?: string;
|
|
};
|
|
|
|
type GpuState = {
|
|
settings: GpuSettings;
|
|
action?: KeyAction<GpuSettings>;
|
|
unsubscribe?: () => void;
|
|
lastSvg?: string;
|
|
};
|
|
|
|
@action({ UUID: "com.bryce.llamawatch.gpu" })
|
|
export class GpuGraph extends SingletonAction<GpuSettings> {
|
|
private states = new Map<string, GpuState>();
|
|
|
|
private stateFor(action: KeyAction<GpuSettings>): GpuState {
|
|
let state = this.states.get(action.id);
|
|
if (!state) {
|
|
state = { settings: {} };
|
|
this.states.set(action.id, state);
|
|
}
|
|
return state;
|
|
}
|
|
|
|
override onWillAppear(ev: WillAppearEvent<GpuSettings>): void {
|
|
if (!ev.action.isKey()) return;
|
|
const state = this.stateFor(ev.action);
|
|
state.settings = ev.payload.settings;
|
|
state.action = ev.action;
|
|
runtime.ensureConnections(cfgFromSettings(state.settings));
|
|
state.unsubscribe = runtime.subscribe(() => this.render(state));
|
|
this.render(state);
|
|
}
|
|
|
|
override onWillDisappear(ev: WillDisappearEvent<GpuSettings>): void {
|
|
const state = this.states.get(ev.action.id);
|
|
if (!state) return;
|
|
state.unsubscribe?.();
|
|
this.states.delete(ev.action.id);
|
|
}
|
|
|
|
override onDidReceiveSettings(ev: DidReceiveSettingsEvent<GpuSettings>): void {
|
|
if (!ev.action.isKey()) return;
|
|
const state = this.stateFor(ev.action);
|
|
state.settings = ev.payload.settings;
|
|
state.action = ev.action;
|
|
runtime.ensureConnections(cfgFromSettings(state.settings));
|
|
this.render(state);
|
|
}
|
|
|
|
override onKeyDown(ev: KeyDownEvent<GpuSettings>): void {
|
|
void streamDeck.system.openUrl(`${cfgFromSettings(ev.payload.settings).baseUrl}/ui`);
|
|
}
|
|
|
|
private render(state: GpuState): void {
|
|
const action = state.action;
|
|
if (!action) return;
|
|
const poller = runtime.pollerInstance;
|
|
const metric = state.settings.metric ?? "util_percent";
|
|
const combo = parseCombo(state.settings.gpuCombo);
|
|
const offline = !poller || poller.isOffline();
|
|
|
|
let value: number | undefined;
|
|
let history: number[] = [];
|
|
let gpuName: string;
|
|
if (combo) {
|
|
const known = new Set((poller?.gpus() ?? []).map((g) => g.id));
|
|
const ids = combo.filter((id) => known.has(id));
|
|
const histories = ids.map((id) => poller?.getHistory(id, metric) ?? []);
|
|
const combined = combineSeries(histories, metric);
|
|
value = combined.value;
|
|
history = combined.history;
|
|
gpuName = ids.length > 0 ? `GPU ${ids.join("+")}` : "NO GPUS";
|
|
} else {
|
|
const gpuId = state.settings.gpuId ?? "all";
|
|
value = poller?.getValue(gpuId, metric);
|
|
history = poller?.getHistory(gpuId, metric) ?? [];
|
|
gpuName = gpuId === "all" ? "ALL GPUS" : this.gpuLabel(gpuId);
|
|
}
|
|
|
|
const svg = renderGpuGraph({ gpuName, metric, value, history, offline });
|
|
if (svg === state.lastSvg) return;
|
|
state.lastSvg = svg;
|
|
void action.setImage(svgDataUrl(svg));
|
|
}
|
|
|
|
private gpuLabel(gpuId: string): string {
|
|
const gpu = runtime.pollerInstance?.gpus().find((g) => g.id === gpuId);
|
|
return gpu && gpu.name ? `GPU ${gpuId} · ${gpu.name}` : `GPU ${gpuId}`;
|
|
}
|
|
}
|
|
|
|
function parseCombo(raw: string | undefined): string[] | undefined {
|
|
const ids = [...new Set((raw ?? "").split(",").map((s) => s.trim()).filter(Boolean))];
|
|
return ids.length > 0 ? ids : undefined;
|
|
}
|