All checks were successful
Dev Build / build-test (pull_request) Successful in 1m29s
- Moonraker REST client with GetPrinterInfo, GetPrintStats, GetPrintHistory - Moonraker WebSocket client with auto-reconnect + telemetry parsing - MQTT client via paho.mqtt.golang with TLS support for Bambu Lab - Moonraker poller worker: background polling, dedup, usage logging to PostgreSQL - MQTT subscriber worker: Bambu telemetry parsing, print job tracking - Config: 7 new env vars (MOONRAKER_URL, MQTT_BROKER, etc.) - main.go: per-printer worker discovery, graceful shutdown
172 lines
6.0 KiB
Go
172 lines
6.0 KiB
Go
// Package clients provides client implementations for printer integrations:
|
|
// Moonraker REST + WebSocket (Klipper-based printers) and MQTT (Bambu Lab).
|
|
package clients
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ── Moonraker response types ────────────────────────────────────────────────
|
|
|
|
// moonrakerRPC is the generic JSON-RPC wrapper Moonraker uses for responses.
|
|
type moonrakerRPC struct {
|
|
Result json.RawMessage `json:"result"`
|
|
Error *moonrakerError `json:"error"`
|
|
}
|
|
|
|
type moonrakerError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// ── Public DTOs ─────────────────────────────────────────────────────────────
|
|
|
|
// MoonrakerPrinterInfo represents the /printer/info response.
|
|
type MoonrakerPrinterInfo struct {
|
|
State string `json:"state"`
|
|
StateMessage string `json:"state_message"`
|
|
KlippyReady bool `json:"klippy_ready"`
|
|
}
|
|
|
|
// MoonrakerPrintStats represents the print_stats object from
|
|
// /printer/objects/query?print_stats.
|
|
type MoonrakerPrintStats struct {
|
|
State string `json:"state"`
|
|
Filename *string `json:"filename"`
|
|
FilamentUsedMm float64 `json:"filament_used"`
|
|
PrintDuration float64 `json:"print_duration"`
|
|
Message *string `json:"message"`
|
|
}
|
|
|
|
// MoonrakerPrintJob represents a single entry in /server/history/items.
|
|
type MoonrakerPrintJob struct {
|
|
JobID string `json:"job_id"`
|
|
Filename string `json:"filename"`
|
|
Status string `json:"status"`
|
|
FilamentUsedMm float64 `json:"filament_used"`
|
|
PrintDuration float64 `json:"print_duration"`
|
|
TotalDuration float64 `json:"total_duration"`
|
|
StartTime *float64 `json:"start_time"`
|
|
EndTime *float64 `json:"end_time"`
|
|
Metadata map[string]interface{} `json:"metadata"`
|
|
}
|
|
|
|
// MoonrakerHistoryResponse wraps the /server/history/items response.
|
|
type MoonrakerHistoryResponse struct {
|
|
Items []MoonrakerPrintJob `json:"items"`
|
|
TotalCount int `json:"count"`
|
|
}
|
|
|
|
// ── Client ──────────────────────────────────────────────────────────────────
|
|
|
|
// MoonrakerClient is an HTTP client for the Moonraker REST API on
|
|
// Klipper-based printers (e.g., Elegoo Centauri Carbon).
|
|
type MoonrakerClient struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewMoonrakerClient creates a MoonrakerClient that targets the given
|
|
// base URL (e.g., "http://192.168.1.50:7125"). The internal HTTP client
|
|
// uses a 15-second timeout.
|
|
func NewMoonrakerClient(baseURL string) *MoonrakerClient {
|
|
baseURL = strings.TrimRight(baseURL, "/")
|
|
return &MoonrakerClient{
|
|
baseURL: baseURL,
|
|
httpClient: &http.Client{
|
|
Timeout: 15 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// GetPrinterInfo calls GET /printer/info and returns the Klipper state.
|
|
// Returns nil when the printer is unreachable or the response cannot be parsed.
|
|
func (c *MoonrakerClient) GetPrinterInfo(ctx context.Context) (*MoonrakerPrinterInfo, error) {
|
|
var info MoonrakerPrinterInfo
|
|
if err := c.getJSON(ctx, "/printer/info", &info); err != nil {
|
|
return nil, err
|
|
}
|
|
return &info, nil
|
|
}
|
|
|
|
// GetPrintStats calls GET /printer/objects/query?print_stats and returns
|
|
// real-time print statistics including filament consumption.
|
|
// Returns nil when no print is active or the printer is unreachable.
|
|
func (c *MoonrakerClient) GetPrintStats(ctx context.Context) (*MoonrakerPrintStats, error) {
|
|
var stats MoonrakerPrintStats
|
|
// Moonraker wraps the object in status.print_stats
|
|
var wrapper struct {
|
|
Status struct {
|
|
PrintStats MoonrakerPrintStats `json:"print_stats"`
|
|
} `json:"status"`
|
|
}
|
|
if err := c.getJSON(ctx, "/printer/objects/query?print_stats", &wrapper); err != nil {
|
|
return nil, err
|
|
}
|
|
stats = wrapper.Status.PrintStats
|
|
return &stats, nil
|
|
}
|
|
|
|
// GetPrintHistory calls GET /server/history/items and returns recent print
|
|
// jobs. limit controls the maximum number of items (clamped 1-100).
|
|
func (c *MoonrakerClient) GetPrintHistory(ctx context.Context, limit int) (*MoonrakerHistoryResponse, error) {
|
|
if limit < 1 {
|
|
limit = 1
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
var history MoonrakerHistoryResponse
|
|
if err := c.getJSON(ctx, fmt.Sprintf("/server/history/items?limit=%d", limit), &history); err != nil {
|
|
return nil, err
|
|
}
|
|
return &history, nil
|
|
}
|
|
|
|
// ── Internal helpers ────────────────────────────────────────────────────────
|
|
|
|
func (c *MoonrakerClient) getJSON(ctx context.Context, path string, target interface{}) error {
|
|
url := c.baseURL + path
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("moonraker: failed to build request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("moonraker: request failed (%s): %w", url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("moonraker: failed to read body: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("moonraker: %s returned HTTP %d: %s", url, resp.StatusCode, string(body))
|
|
}
|
|
|
|
// Moonraker wraps responses in {"result": ...}
|
|
var rpc moonrakerRPC
|
|
if err := json.Unmarshal(body, &rpc); err != nil {
|
|
return fmt.Errorf("moonraker: failed to parse response: %w", err)
|
|
}
|
|
if rpc.Error != nil && rpc.Error.Message != "" {
|
|
return fmt.Errorf("moonraker: api error: %s", rpc.Error.Message)
|
|
}
|
|
|
|
if err := json.Unmarshal(rpc.Result, target); err != nil {
|
|
return fmt.Errorf("moonraker: failed to unmarshal result: %w (raw: %s)", err, string(rpc.Result))
|
|
}
|
|
return nil
|
|
}
|