teg/backend/milestone_cameras.go
2026-06-09 14:26:35 +02:00

458 lines
13 KiB
Go

// backend\milestone_cameras.go
package main
import (
"context"
"errors"
"net/http"
"strings"
"github.com/jackc/pgx/v5"
)
type milestoneCameraPatchDevice struct {
DeviceID string
InventoryNumber string
ChildID string
MilestoneHardwareID string
MilestoneDeviceID string
DeviceType string
Enabled bool
RecordingEnabled bool
}
type milestoneRecordingStorageRef struct {
ID string `json:"id"`
DisplayName string `json:"displayName,omitempty"`
Name string `json:"name,omitempty"`
DiskPath string `json:"diskPath,omitempty"`
Mounted bool `json:"mounted,omitempty"`
}
type milestoneCameraStream struct {
Index int `json:"index"`
Codec string `json:"codec"`
FPS *int `json:"fps"`
JPEGQuality *int `json:"jPEGQuality"`
Quality *int `json:"quality"`
Resolution string `json:"resolution"`
EdgeStorageSupported bool `json:"edgeStorageSupported"`
}
type milestoneCameraDetail struct {
ID string `json:"id"`
MilestoneDeviceID string `json:"milestoneDeviceId"`
MilestoneHardwareID string `json:"milestoneHardwareId"`
Enabled bool `json:"enabled"`
RecordingEnabled bool `json:"recordingEnabled"`
RecordingFramerate *int `json:"recordingFramerate"`
RecordKeyframesOnly bool `json:"recordKeyframesOnly"`
RecordingStorage *milestoneRecordingStorageRef `json:"recordingStorage,omitempty"`
ArchiveStorages []milestoneArchiveStorage `json:"archiveStorages"`
Streams []milestoneCameraStream `json:"streams"`
}
type updateMilestoneCameraStreamRequest struct {
Index int `json:"index"`
Codec string `json:"codec"`
FPS *int `json:"fps"`
JPEGQuality *int `json:"jPEGQuality"`
Quality *int `json:"quality"`
Resolution string `json:"resolution"`
}
type updateMilestoneCameraDetailsRequest struct {
Enabled *bool `json:"enabled"`
RecordingEnabled *bool `json:"recordingEnabled"`
RecordingFramerate *int `json:"recordingFramerate"`
RecordKeyframesOnly *bool `json:"recordKeyframesOnly"`
RecordingStorageID *string `json:"recordingStorageId"`
Streams []updateMilestoneCameraStreamRequest `json:"streams"`
}
func (s *Server) getMilestoneCameraPatchDevice(
ctx context.Context,
deviceID string,
childID string,
) (milestoneCameraPatchDevice, error) {
var camera milestoneCameraPatchDevice
err := s.db.QueryRow(
ctx,
`
SELECT
devices.id::TEXT,
devices.inventory_number,
milestone_hardware_child_devices.id::TEXT,
milestone_hardware_child_devices.milestone_hardware_id,
milestone_hardware_child_devices.milestone_device_id,
milestone_hardware_child_devices.device_type,
milestone_hardware_child_devices.enabled,
COALESCE(milestone_hardware_child_devices.recording_enabled, false)
FROM milestone_hardware_child_devices
INNER JOIN devices
ON devices.milestone_hardware_id = milestone_hardware_child_devices.milestone_hardware_id
WHERE devices.id = $1
AND milestone_hardware_child_devices.id = $2
LIMIT 1
`,
deviceID,
childID,
).Scan(
&camera.DeviceID,
&camera.InventoryNumber,
&camera.ChildID,
&camera.MilestoneHardwareID,
&camera.MilestoneDeviceID,
&camera.DeviceType,
&camera.Enabled,
&camera.RecordingEnabled,
)
return camera, err
}
func (s *Server) handleGetMilestoneCameraDetails(w http.ResponseWriter, r *http.Request) {
deviceID := strings.TrimSpace(r.PathValue("id"))
childID := strings.TrimSpace(r.PathValue("childId"))
if deviceID == "" || childID == "" {
writeError(w, http.StatusBadRequest, "Geräte-ID oder Child-ID fehlt")
return
}
cameraDevice, err := s.getMilestoneCameraPatchDevice(r.Context(), deviceID, childID)
if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "Milestone-Kamera wurde nicht gefunden")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "Milestone-Kamera konnte nicht geladen werden")
return
}
if strings.TrimSpace(strings.ToLower(cameraDevice.DeviceType)) != "camera" &&
strings.TrimSpace(strings.ToLower(cameraDevice.DeviceType)) != "cameras" {
writeError(w, http.StatusBadRequest, "Das Child-Device ist keine Kamera")
return
}
config, err := s.getMilestoneRuntimeConfig(r.Context())
if err != nil {
writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden")
return
}
cameraData, err := getMilestoneNamedDevice(
r.Context(),
config,
"cameras",
cameraDevice.MilestoneDeviceID,
)
if err != nil {
writeError(w, http.StatusBadGateway, "Milestone-Kamera konnte nicht geladen werden")
return
}
archiveStorages := []milestoneArchiveStorage{}
var currentStorage *milestoneStorage
if recordingStorage, ok := cameraData["recordingStorage"].(map[string]any); ok {
recordingStorageID := strings.TrimSpace(milestoneStringValue(recordingStorage["id"]))
if recordingStorageID != "" {
currentStorage, err = getMilestoneStorage(
r.Context(),
config,
recordingStorageID,
)
if err != nil {
writeError(w, http.StatusBadGateway, "Milestone-Storage konnte nicht geladen werden")
return
}
archiveStorages, err = getMilestoneArchiveStorages(
r.Context(),
config,
recordingStorageID,
)
if err != nil {
writeError(w, http.StatusBadGateway, "Milestone-Archive-Storage konnte nicht geladen werden")
return
}
}
}
camera := mapMilestoneCameraDetail(cameraData, cameraDevice, currentStorage, archiveStorages)
writeJSON(w, http.StatusOK, map[string]any{
"camera": camera,
})
}
func (s *Server) handleUpdateMilestoneCameraDetails(w http.ResponseWriter, r *http.Request) {
deviceID := strings.TrimSpace(r.PathValue("id"))
childID := strings.TrimSpace(r.PathValue("childId"))
if deviceID == "" || childID == "" {
writeError(w, http.StatusBadRequest, "Geräte-ID oder Child-ID fehlt")
return
}
cameraDevice, err := s.getMilestoneCameraPatchDevice(r.Context(), deviceID, childID)
if errors.Is(err, pgx.ErrNoRows) {
writeError(w, http.StatusNotFound, "Milestone-Kamera wurde nicht gefunden")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "Milestone-Kamera konnte nicht geladen werden")
return
}
if strings.TrimSpace(strings.ToLower(cameraDevice.DeviceType)) != "camera" &&
strings.TrimSpace(strings.ToLower(cameraDevice.DeviceType)) != "cameras" {
writeError(w, http.StatusBadRequest, "Das Child-Device ist keine Kamera")
return
}
var input updateMilestoneCameraDetailsRequest
if err := readJSON(r, &input); err != nil {
writeError(w, http.StatusBadRequest, "Invalid JSON")
return
}
config, err := s.getMilestoneRuntimeConfig(r.Context())
if err != nil {
writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden")
return
}
currentCamera, err := getMilestoneNamedDevice(
r.Context(),
config,
"cameras",
cameraDevice.MilestoneDeviceID,
)
if err != nil {
writeError(w, http.StatusBadGateway, "Milestone-Kamera konnte nicht geladen werden")
return
}
if input.Enabled != nil {
currentCamera["enabled"] = *input.Enabled
}
if input.RecordingEnabled != nil {
currentCamera["recordingEnabled"] = *input.RecordingEnabled
}
if input.RecordingFramerate != nil {
currentCamera["recordingFramerate"] = *input.RecordingFramerate
}
if input.RecordKeyframesOnly != nil {
currentCamera["recordKeyframesOnly"] = *input.RecordKeyframesOnly
}
if input.RecordingStorageID != nil {
recordingStorageID := strings.TrimSpace(*input.RecordingStorageID)
if recordingStorageID == "" {
currentCamera["recordingStorage"] = nil
} else {
currentCamera["recordingStorage"] = map[string]any{
"type": "storages",
"id": recordingStorageID,
}
}
}
applyMilestoneCameraStreamUpdates(currentCamera, input.Streams)
if err := patchMilestoneNamedDevice(
r.Context(),
config,
"cameras",
cameraDevice.MilestoneDeviceID,
currentCamera,
); err != nil {
writeError(w, http.StatusBadGateway, "Milestone-Kamera konnte nicht aktualisiert werden")
return
}
_, err = s.db.Exec(
r.Context(),
`
UPDATE milestone_hardware_child_devices
SET
enabled = COALESCE($2, enabled),
recording_enabled = COALESCE($3, recording_enabled),
recording_storage_id = COALESCE($4, recording_storage_id),
updated_at = now()
WHERE id = $1
`,
cameraDevice.ChildID,
input.Enabled,
input.RecordingEnabled,
input.RecordingStorageID,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Milestone-Kamera konnte lokal nicht aktualisiert werden")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
})
}
func mapMilestoneCameraDetail(
cameraData map[string]any,
cameraDevice milestoneCameraPatchDevice,
currentStorage *milestoneStorage,
archiveStorages []milestoneArchiveStorage,
) milestoneCameraDetail {
detail := milestoneCameraDetail{
ID: cameraDevice.ChildID,
MilestoneDeviceID: cameraDevice.MilestoneDeviceID,
MilestoneHardwareID: cameraDevice.MilestoneHardwareID,
Enabled: milestoneBoolValue(cameraData["enabled"]),
RecordingEnabled: milestoneBoolValue(cameraData["recordingEnabled"]),
RecordKeyframesOnly: milestoneBoolValue(cameraData["recordKeyframesOnly"]),
Streams: extractMilestoneCameraStreams(cameraData),
ArchiveStorages: archiveStorages,
}
if value, ok := milestoneIntPointerValue(cameraData["recordingFramerate"]); ok {
detail.RecordingFramerate = value
}
if currentStorage != nil && strings.TrimSpace(currentStorage.ID) != "" {
detail.RecordingStorage = &milestoneRecordingStorageRef{
ID: currentStorage.ID,
DisplayName: currentStorage.DisplayName,
Name: currentStorage.Name,
DiskPath: currentStorage.DiskPath,
Mounted: currentStorage.Mounted,
}
} else if recordingStorage, ok := cameraData["recordingStorage"].(map[string]any); ok {
recordingStorageID := strings.TrimSpace(milestoneStringValue(recordingStorage["id"]))
if recordingStorageID != "" {
detail.RecordingStorage = &milestoneRecordingStorageRef{
ID: recordingStorageID,
}
}
}
return detail
}
func extractMilestoneCameraStreams(cameraData map[string]any) []milestoneCameraStream {
settings, ok := cameraData["settings"].([]any)
if !ok {
return []milestoneCameraStream{}
}
streams := []milestoneCameraStream{}
for _, rawSetting := range settings {
setting, ok := rawSetting.(map[string]any)
if !ok {
continue
}
rawStreams, ok := setting["stream"].([]any)
if !ok {
continue
}
for index, rawStream := range rawStreams {
streamMap, ok := rawStream.(map[string]any)
if !ok {
continue
}
stream := milestoneCameraStream{
Index: index,
Codec: milestoneStringValue(streamMap["codec"]),
Resolution: milestoneStringValue(streamMap["resolution"]),
EdgeStorageSupported: milestoneBoolValue(streamMap["edgeStorageSupported"]),
}
if value, ok := milestoneIntPointerValue(streamMap["FPS"]); ok {
stream.FPS = value
}
if value, ok := milestoneIntPointerValue(streamMap["jPEGQuality"]); ok {
stream.JPEGQuality = value
}
if value, ok := milestoneIntPointerValue(streamMap["quality"]); ok {
stream.Quality = value
}
streams = append(streams, stream)
}
}
return streams
}
func applyMilestoneCameraStreamUpdates(
cameraData map[string]any,
streamUpdates []updateMilestoneCameraStreamRequest,
) {
if len(streamUpdates) == 0 {
return
}
settings, ok := cameraData["settings"].([]any)
if !ok {
return
}
updatesByIndex := map[int]updateMilestoneCameraStreamRequest{}
for _, streamUpdate := range streamUpdates {
updatesByIndex[streamUpdate.Index] = streamUpdate
}
for _, rawSetting := range settings {
setting, ok := rawSetting.(map[string]any)
if !ok {
continue
}
rawStreams, ok := setting["stream"].([]any)
if !ok {
continue
}
for index, rawStream := range rawStreams {
streamMap, ok := rawStream.(map[string]any)
if !ok {
continue
}
update, exists := updatesByIndex[index]
if !exists {
continue
}
streamMap["codec"] = update.Codec
streamMap["resolution"] = update.Resolution
if update.FPS != nil {
streamMap["FPS"] = *update.FPS
}
if update.JPEGQuality != nil {
streamMap["jPEGQuality"] = *update.JPEGQuality
}
if update.Quality != nil {
streamMap["quality"] = *update.Quality
}
}
}
}