Files
Joshua King b1ed8cdb20
Build (Dev) / build (push) Successful in 11s
CI / quality (push) Successful in 11s
CI / quality (pull_request) Successful in 11s
hub: emit battery_pct/video_remaining as null, not omitted
The SPA types these as number|null and null-checks them, but omitempty
dropped the field entirely when uncalibrated → undefined in JS → "NaN%".
Always serialize the field (null when unknown) so the card shows "N/A".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 20:22:06 -04:00

95 lines
3.1 KiB
Go

// Package models contains the data structures for RemoteRig.
package models
import (
"time"
)
// Camera represents a registered GoPro camera in the system.
type Camera struct {
CameraID string `json:"camera_id"`
FriendlyName string `json:"friendly_name"`
MacAddress *string `json:"mac_address,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// StatusLog records a single status poll from an ESP8266 node.
type StatusLog struct {
ID int64 `json:"id"`
CameraID string `json:"camera_id"`
RecordedAt time.Time `json:"recorded_at"`
BatteryPct *int `json:"battery_pct,omitempty"`
VideoRemainingSec *int `json:"video_remaining_sec,omitempty"`
RecordingState int `json:"recording_state"` // 0=idle, 1=recording
Mode *string `json:"mode,omitempty"`
Resolution *string `json:"resolution,omitempty"`
FPS *int `json:"fps,omitempty"`
Online int `json:"online"` // 0=offline, 1=online
RawBatteryPct *float64 `json:"raw_battery_pct,omitempty"`
}
// CameraStatus is a combined view: latest status log joined with camera info.
type CameraStatus struct {
CameraID string `json:"camera_id"`
FriendlyName string `json:"friendly_name"`
// Not omitempty: the SPA expects these as `number | null`. Omitting them
// makes the field `undefined` in JS, which slips past null checks and
// renders as "NaN%".
BatteryPct *int `json:"battery_pct"`
VideoRemainingSec *int `json:"video_remaining_sec"`
Recording bool `json:"recording"`
Mode string `json:"mode"`
Resolution string `json:"resolution"`
FPS int `json:"fps"`
Online bool `json:"online"`
LastSeen time.Time `json:"last_seen"`
}
// RecordingEvent represents a start/stop recording event.
type RecordingEvent struct {
ID int64 `json:"id"`
CameraID string `json:"camera_id"`
StartedAt time.Time `json:"started_at"`
StoppedAt *time.Time `json:"stopped_at,omitempty"`
Reason *string `json:"reason,omitempty"`
Duration *int `json:"duration,omitempty"` // seconds
}
// Settings stores system-wide configuration.
type Setting struct {
Key string `json:"key"`
Value string `json:"value"`
UpdatedAt time.Time `json:"updated_at"`
}
// NewCameraStatus creates a CameraStatus from a Camera and StatusLog.
func NewCameraStatus(c Camera, sl StatusLog) CameraStatus {
return CameraStatus{
CameraID: c.CameraID,
FriendlyName: c.FriendlyName,
BatteryPct: sl.BatteryPct,
VideoRemainingSec: sl.VideoRemainingSec,
Recording: sl.RecordingState == 1,
Mode: takeString(sl.Mode),
Resolution: takeString(sl.Resolution),
FPS: takeInt(sl.FPS),
Online: sl.Online == 1,
LastSeen: sl.RecordedAt,
}
}
func takeString(s *string) string {
if s == nil {
return ""
}
return *s
}
func takeInt(i *int) int {
if i == nil {
return 0
}
return *i
}