458 lines
13 KiB
Go
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
|
|
}
|
|
}
|
|
}
|
|
}
|