- Add internal/clients/moonraker.go: HTTP client for Moonraker REST API - Add internal/clients/mqtt.go: MQTT client via paho.mqtt.golang with TLS support - Add internal/workers/moonraker_poller.go: background polling, usage logging, SSE broadcasts - Add internal/workers/mqtt_subscriber.go: per-printer MQTT subscriber, Bambu telemetry parsing, SSE broadcasts - Wire workers into cmd/server/main.go (Start/Stop with graceful shutdown) Blocked at step 4: go toolchain not installed on build host
162 lines
5.1 KiB
Go
162 lines
5.1 KiB
Go
// Package clients provides third-party printer integrations.
|
|
package clients
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// MoonrakerPrinterInfo represents the response from /api/printer/info.
|
|
type MoonrakerPrinterInfo struct {
|
|
State string `json:"state"`
|
|
Hostname string `json:"hostname,omitempty"`
|
|
SoftwareVersion string `json:"software_version,omitempty"`
|
|
}
|
|
|
|
// MoonrakerPrintStats represents the response from /api/printer/print_stats.
|
|
type MoonrakerPrintStats struct {
|
|
State string `json:"state"`
|
|
Filename string `json:"filename,omitempty"`
|
|
FilamentUsedMm float64 `json:"filament_used,omitempty"`
|
|
TotalDuration float64 `json:"total_duration,omitempty"`
|
|
PrintDuration float64 `json:"print_duration,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// MoonrakerPrintJob represents a single job from the history API.
|
|
type MoonrakerPrintJob struct {
|
|
JobID string `json:"job_id,omitempty"`
|
|
Filename string `json:"filename"`
|
|
Status string `json:"status"`
|
|
StartTime time.Time `json:"start_time"`
|
|
EndTime time.Time `json:"end_time,omitempty"`
|
|
FilamentUsedMm float64 `json:"filament_used,omitempty"`
|
|
TotalDuration float64 `json:"total_duration,omitempty"`
|
|
}
|
|
|
|
// MoonrakerHistoryResponse represents the response from /api/server/history/job.
|
|
type MoonrakerHistoryResponse struct {
|
|
Items []MoonrakerPrintJob `json:"jobs"`
|
|
}
|
|
|
|
// MoonrakerClient is an HTTP client for the Moonraker API.
|
|
type MoonrakerClient struct {
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
// NewMoonrakerClient creates a MoonrakerClient with the given request timeout.
|
|
func NewMoonrakerClient(timeout time.Duration) *MoonrakerClient {
|
|
return &MoonrakerClient{
|
|
HTTPClient: &http.Client{Timeout: timeout},
|
|
}
|
|
}
|
|
|
|
// baseURL builds the Moonraker base URL from host and port.
|
|
func (c *MoonrakerClient) baseURL(host string, port int) string {
|
|
if port == 0 {
|
|
port = 80
|
|
}
|
|
return fmt.Sprintf("http://%s:%d", host, port)
|
|
}
|
|
|
|
// GetPrinterInfo fetches printer info from Moonraker.
|
|
func (c *MoonrakerClient) GetPrinterInfo(ctx context.Context, host string, port int, apiKey string) (*MoonrakerPrinterInfo, error) {
|
|
url := c.baseURL(host, port) + "/api/printer/info"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if apiKey != "" {
|
|
req.Header.Set("X-Api-Key", apiKey)
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("moonraker getPrinterInfo request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("moonraker getPrinterInfo returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var body struct {
|
|
Result MoonrakerPrinterInfo `json:"result"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
return nil, fmt.Errorf("moonraker getPrinterInfo decode failed: %w", err)
|
|
}
|
|
|
|
slog.Debug("moonraker printer info", "host", host, "state", body.Result.State)
|
|
return &body.Result, nil
|
|
}
|
|
|
|
// GetPrintStats fetches current print statistics from Moonraker.
|
|
func (c *MoonrakerClient) GetPrintStats(ctx context.Context, host string, port int, apiKey string) (*MoonrakerPrintStats, error) {
|
|
url := c.baseURL(host, port) + "/api/printer/print_stats"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if apiKey != "" {
|
|
req.Header.Set("X-Api-Key", apiKey)
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("moonraker getPrintStats request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("moonraker getPrintStats returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var body struct {
|
|
Result MoonrakerPrintStats `json:"result"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
return nil, fmt.Errorf("moonraker getPrintStats decode failed: %w", err)
|
|
}
|
|
|
|
slog.Debug("moonraker print stats", "host", host, "state", body.Result.State, "filename", body.Result.Filename)
|
|
return &body.Result, nil
|
|
}
|
|
|
|
// GetPrintHistory fetches completed print job history from Moonraker.
|
|
func (c *MoonrakerClient) GetPrintHistory(ctx context.Context, host string, port int, apiKey string, limit int) (*MoonrakerHistoryResponse, error) {
|
|
if limit <= 0 {
|
|
limit = 25
|
|
}
|
|
url := fmt.Sprintf("%s/api/server/history/job?limit=%d", c.baseURL(host, port), limit)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if apiKey != "" {
|
|
req.Header.Set("X-Api-Key", apiKey)
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("moonraker getPrintHistory request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("moonraker getPrintHistory returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var body MoonrakerHistoryResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
return nil, fmt.Errorf("moonraker getPrintHistory decode failed: %w", err)
|
|
}
|
|
|
|
slog.Debug("moonraker print history", "host", host, "count", len(body.Items))
|
|
return &body, nil
|
|
}
|