package statusreporter import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strings" "time" ) type Snapshot struct { Status string ServerConnectivity string ScreenID string ServerBaseURL string MQTTBroker string HeartbeatEverySeconds int StartedAt time.Time LastHeartbeatAt time.Time DisplayState string } type statusPayload struct { ScreenID string `json:"screen_id"` Timestamp string `json:"ts"` Status string `json:"status"` ServerConnectivity string `json:"server_connectivity"` ServerURL string `json:"server_url"` MQTTBroker string `json:"mqtt_broker"` HeartbeatEverySeconds int `json:"heartbeat_every_seconds"` StartedAt string `json:"started_at,omitempty"` LastHeartbeatAt string `json:"last_heartbeat_at,omitempty"` DisplayState string `json:"display_state,omitempty"` } // MQTTConfig holds the MQTT broker configuration returned by the server in the // status-report response. All fields are empty when the server did not send // a mqtt object (e.g. MQTT is disabled on the server side). type MQTTConfig struct { Broker string Username string Password string } // serverResponse is the JSON body returned by POST /api/v1/player/status. type serverResponse struct { Status string `json:"status"` MQTT *serverMQTTBlock `json:"mqtt,omitempty"` } type serverMQTTBlock struct { Broker string `json:"broker"` Username string `json:"username"` Password string `json:"password"` } type Reporter struct { baseURL string client *http.Client now func() time.Time } func New(baseURL string, client *http.Client, now func() time.Time) *Reporter { if client == nil { client = &http.Client{Timeout: 5 * time.Second} } if now == nil { now = time.Now } return &Reporter{ baseURL: strings.TrimRight(baseURL, "/"), client: client, now: now, } } // Send reports the snapshot to the server and returns the MQTT configuration // provided by the server in the response body. If the server does not include // a mqtt object the returned MQTTConfig will have an empty Broker field. func (r *Reporter) Send(ctx context.Context, snapshot Snapshot) (MQTTConfig, error) { payload := buildPayload(snapshot, r.now()) body, err := json.Marshal(payload) if err != nil { return MQTTConfig{}, fmt.Errorf("marshal status payload: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.baseURL+"/api/v1/player/status", bytes.NewReader(body)) if err != nil { return MQTTConfig{}, fmt.Errorf("build status request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := r.client.Do(req) if err != nil { return MQTTConfig{}, fmt.Errorf("send status request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return MQTTConfig{}, fmt.Errorf("unexpected status response: %s", resp.Status) } var sr serverResponse if err := json.NewDecoder(resp.Body).Decode(&sr); err != nil { // Non-fatal: decoding failure just means no MQTT config update. return MQTTConfig{}, nil } if sr.MQTT != nil { return MQTTConfig{ Broker: sr.MQTT.Broker, Username: sr.MQTT.Username, Password: sr.MQTT.Password, }, nil } return MQTTConfig{}, nil } func buildPayload(snapshot Snapshot, now time.Time) statusPayload { payload := statusPayload{ ScreenID: snapshot.ScreenID, Timestamp: now.Format(time.RFC3339), Status: snapshot.Status, ServerConnectivity: snapshot.ServerConnectivity, ServerURL: snapshot.ServerBaseURL, MQTTBroker: snapshot.MQTTBroker, HeartbeatEverySeconds: snapshot.HeartbeatEverySeconds, } if !snapshot.StartedAt.IsZero() { payload.StartedAt = snapshot.StartedAt.Format(time.RFC3339) } if !snapshot.LastHeartbeatAt.IsZero() { payload.LastHeartbeatAt = snapshot.LastHeartbeatAt.Format(time.RFC3339) } if snapshot.DisplayState != "" { payload.DisplayState = snapshot.DisplayState } return payload }