package device import ( "fmt" "log/slog" routeros "github.com/go-routeros/routeros/v3" ) // HealthMetrics holds system resource metrics collected from a RouterOS device. // String fields match the raw RouterOS API values so the subscriber can parse // and validate them before inserting into TimescaleDB. type HealthMetrics struct { CPULoad string `json:"cpu_load"` FreeMemory string `json:"free_memory"` TotalMemory string `json:"total_memory"` FreeDisk string `json:"free_disk"` TotalDisk string `json:"total_disk"` Temperature string `json:"temperature"` // empty string if device has no sensor } // CollectHealth gathers system health metrics for a RouterOS device. // // It combines data already present in DeviceInfo (CPU, memory) with additional // disk stats from /system/resource/print and temperature from /system/health/print. // // Temperature handling: // - RouterOS v7: /system/health/print returns rows with name/value columns; // looks for "cpu-temperature" then "board-temperature" as a fallback. // - RouterOS v6: /system/health/print returns a flat map; looks for // "cpu-temperature" key directly. // - If the command fails or no temperature key is found, Temperature is set to "". func CollectHealth(client *routeros.Client, info DeviceInfo) (HealthMetrics, error) { health := HealthMetrics{ CPULoad: info.CPULoad, FreeMemory: info.FreeMemory, TotalMemory: info.TotalMemory, } // Collect disk stats (not included in the default /system/resource/print proplist // used by DetectVersion, so we query explicitly here). diskReply, err := client.Run( "/system/resource/print", "=.proplist=free-hdd-space,total-hdd-space", ) if err != nil { slog.Warn("could not collect disk stats", "error", err) } else if len(diskReply.Re) > 0 { m := diskReply.Re[0].Map health.FreeDisk = m["free-hdd-space"] health.TotalDisk = m["total-hdd-space"] } // Collect temperature from /system/health/print. // This command may not exist on all devices, so errors are non-fatal. health.Temperature = collectTemperature(client, info.MajorVersion) return health, nil } // collectTemperature queries /system/health/print and extracts the temperature // reading. Returns an empty string if the device has no temperature sensor or // the command is not supported. func collectTemperature(client *routeros.Client, majorVersion int) string { reply, err := client.Run("/system/health/print") if err != nil { slog.Debug("temperature collection not available", "error", err) return "" } if len(reply.Re) == 0 { return "" } // RouterOS v7 returns rows with "name" and "value" columns. // RouterOS v6 returns a flat map in a single sentence. if majorVersion >= 7 { // v7: iterate rows looking for known temperature keys. var fallback string for _, sentence := range reply.Re { m := sentence.Map name := m["name"] value := m["value"] if name == "cpu-temperature" { return value } if name == "board-temperature" { fallback = value } } return fallback } // v6 (or unknown version): flat map — look for cpu-temperature key directly. m := reply.Re[0].Map if temp, ok := m["cpu-temperature"]; ok { return temp } if temp, ok := m["board-temperature"]; ok { return temp } return "" } // collectHealthError returns an error for CollectHealth callers when the // primary resource query fails completely. func collectHealthError(err error) error { return fmt.Errorf("collecting health metrics: %w", err) }