// 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 } } } }