feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com> Co-authored-by: arturict <arturict@users.noreply.github.com> Co-authored-by: Thomas <thomas@ajnart.fr> Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com> Co-authored-by: Danura30082 <danura30082+github@gmail.com> Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com> Co-authored-by: ajnart <thomas@ajnart.fr> Co-authored-by: ajnart <thomas@ajnart.dev> Co-authored-by: chenjun <chenjun@foxmail.com> Co-authored-by: Wikibear <wikibear@users.noreply.github.com> Co-authored-by: Bergasha <bergasha@gmail.com> Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com> Co-authored-by: Lucas Araujo <lucas.lra@gmail.com> Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com> Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social> Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com> Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com> Co-authored-by: thrawnx <tight.bulb9384@fastmail.com> Co-authored-by: Chris <58918656+nordada@users.noreply.github.com> Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com> Co-authored-by: Yanis <naamaneyanis@gmail.com> Co-authored-by: Wikibear <me@sej.de> Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
This commit is contained in:
3286 files changed
+284386
-139176
No files matched your search
@@ -35,7 +35,11 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni
|
||||
|
||||
if (session == null) throw new Error("Session was unexpectitly null");
|
||||
|
||||
const [stats, cpuTemp] = await Promise.all([this.getAllStatsAsync(), this.getCpuTempAsync()]);
|
||||
const [stats, cpuTemp, smart] = await Promise.all([
|
||||
this.getAllStatsAsync(),
|
||||
this.getCpuTempAsync(),
|
||||
this.getSmartAsync(),
|
||||
]);
|
||||
|
||||
return {
|
||||
cpuUtilization: stats.cpu.total,
|
||||
@@ -57,7 +61,7 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni
|
||||
rebootRequired: false,
|
||||
cpuModelName: stats.quicklook?.cpu_name ?? "Unknown",
|
||||
loadAverage: null,
|
||||
smart: [],
|
||||
smart,
|
||||
cpuTemp,
|
||||
gpu: stats.gpu.map((gpu) => ({
|
||||
gpuId: gpu.gpu_id,
|
||||
@@ -97,6 +101,23 @@ export class GlancesIntegration extends Integration implements ISystemHealthMoni
|
||||
}
|
||||
}
|
||||
|
||||
private async getSmartAsync(): Promise<SystemHealthMonitoring["smart"]> {
|
||||
// The SMART plugin is disabled by default (Glances answers 400), so a missing
|
||||
// or unexpected /api/4/smart response must not fail getSystemInfoAsync().
|
||||
try {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/4/smart"));
|
||||
|
||||
if (!response.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const devices = await smartSchema.parseAsync(await response.json());
|
||||
return devices.flatMap(mapGlancesSmartDevice);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async getAllStatsAsync() {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/4/all"));
|
||||
|
||||
@@ -220,3 +241,74 @@ export const parseGlancesCpuTempFromSensors = (sensors: z.infer<typeof sensorsSc
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Glances (pySMART) returns one object per disk: "DeviceName" is "<device> <model>",
|
||||
// every other object value is a SMART attribute (ATA) or an NVMe health log entry.
|
||||
const smartAttributeSchema = z.object({
|
||||
name: z.string().nullable().optional(),
|
||||
key: z.string().nullable().optional(),
|
||||
value: z.union([z.string(), z.number()]).nullable().optional(),
|
||||
raw: z.union([z.string(), z.number()]).nullable().optional(),
|
||||
when_failed: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
const smartSchema = z.array(z.object({ DeviceName: z.string() }).catchall(z.unknown()));
|
||||
|
||||
const ataTemperatureNames = ["Temperature_Celsius", "Airflow_Temperature_Cel"] as const;
|
||||
|
||||
const mapGlancesSmartDevice = (device: z.infer<typeof smartSchema>[number]): SystemHealthMonitoring["smart"] => {
|
||||
const attributes = Object.values(device).flatMap((value) => {
|
||||
const result = smartAttributeSchema.safeParse(value);
|
||||
return result.success ? [result.data] : [];
|
||||
});
|
||||
|
||||
// ATA: when_failed is "-" unless the attribute crossed its threshold. NVMe: critical warning bitmask.
|
||||
const failedAttributes = attributes
|
||||
.filter((attribute) => attribute.when_failed && attribute.when_failed !== "-")
|
||||
.flatMap((attribute) => attribute.name ?? attribute.key ?? []);
|
||||
const criticalWarningValue = attributes.find((attribute) => attribute.key === "criticalWarning")?.value;
|
||||
const criticalWarningCount = criticalWarningValue == null ? Number.NaN : Number(criticalWarningValue);
|
||||
const isNvme = Number.isFinite(criticalWarningCount);
|
||||
|
||||
// Glances lists a device even when no health attribute was read (or all are hidden by hide_attributes).
|
||||
// Without ATA thresholds or an NVMe critical warning there is no verdict, so the widget shows N/A.
|
||||
const hasAtaThresholds = attributes.some((attribute) => typeof attribute.when_failed === "string");
|
||||
if (!hasAtaThresholds && !isNvme) return [];
|
||||
|
||||
const hasCriticalWarning = isNvme && criticalWarningCount !== 0;
|
||||
const healthy = failedAttributes.length === 0 && !hasCriticalWarning;
|
||||
|
||||
return [
|
||||
{
|
||||
deviceName: `/dev/${device.DeviceName.split(" ")[0]}`,
|
||||
temperature: getSmartTemperature(attributes),
|
||||
overallStatus: healthy ? "PASSED" : "FAILED",
|
||||
healthy,
|
||||
statusReason: getSmartStatusReason(failedAttributes, isNvme, hasCriticalWarning),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const getSmartStatusReason = (
|
||||
failedAttributes: string[],
|
||||
isNvme: boolean,
|
||||
hasCriticalWarning: boolean,
|
||||
): SystemHealthMonitoring["smart"][number]["statusReason"] => {
|
||||
if (failedAttributes.length > 0) return { type: "attributesFailed", attributes: failedAttributes };
|
||||
if (hasCriticalWarning) return { type: "criticalWarning" };
|
||||
return { type: isNvme ? "noCriticalWarning" : "attributesWithinThresholds" };
|
||||
};
|
||||
|
||||
const getSmartTemperature = (attributes: z.infer<typeof smartAttributeSchema>[]): number | null => {
|
||||
const nvmeTemperature = attributes.find((attribute) => attribute.key === "_temperature")?.value;
|
||||
if (typeof nvmeTemperature === "number") return nvmeTemperature;
|
||||
|
||||
for (const name of ataTemperatureNames) {
|
||||
// ATA raw values may carry extra data, e.g. "35 (Min/Max 20/45)".
|
||||
const raw = attributes.find((attribute) => attribute.name === name)?.raw;
|
||||
const temperature = Number.parseInt(String(raw ?? ""), 10);
|
||||
if (!Number.isNaN(temperature)) return temperature;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in new issue
Block a user