teg/backend/milestone_sync.go
2026-06-22 13:57:42 +02:00

1111 lines
26 KiB
Go

// backend\milestone_sync.go
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/jackc/pgx/v5"
)
type milestoneSyncResult struct {
TokenReceived bool `json:"tokenReceived"`
TokenType string `json:"tokenType"`
TokenExpiresAt time.Time `json:"tokenExpiresAt"`
RecordingServerID string `json:"recordingServerId"`
RecordingServerName string `json:"recordingServerName"`
RecordingServerVersion string `json:"recordingServerVersion"`
HardwareCount int `json:"hardwareCount"`
}
type milestoneSyncEvent struct {
Type string `json:"type"`
Message string `json:"message,omitempty"`
TokenType string `json:"tokenType,omitempty"`
TokenExpiresAt *time.Time `json:"tokenExpiresAt,omitempty"`
RecordingServerID string `json:"recordingServerId,omitempty"`
RecordingServerName string `json:"recordingServerName,omitempty"`
RecordingServerVersion string `json:"recordingServerVersion,omitempty"`
HardwareCount int `json:"hardwareCount,omitempty"`
Settings *milestoneSettingsResponse `json:"settings,omitempty"`
}
type milestoneRecordingServersResponse struct {
Array []milestoneRecordingServer `json:"array"`
}
type milestoneRecordingServer struct {
ID string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
Version string `json:"version"`
ProductVersion string `json:"productVersion"`
RecordingServerVersion string `json:"recordingServerVersion"`
}
type milestoneHardwareResponse struct {
Array []milestoneHardware `json:"array"`
}
type milestoneHardware struct {
ID string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
DeviceName string `json:"deviceName"`
Address string `json:"address"`
Enabled *bool `json:"enabled"`
Disabled *bool `json:"disabled"`
Relations milestoneRelations `json:"relations"`
}
type milestoneRelations struct {
Parent milestoneRelationRef `json:"parent"`
Self milestoneRelationRef `json:"self"`
}
type milestoneRelationRef struct {
Type string `json:"type"`
ID string `json:"id"`
}
type milestoneHardwareDriverSettingsResponse struct {
Array []milestoneHardwareDriverSettingsItem `json:"array"`
}
type milestoneHardwareDriverSettingsItem struct {
DisplayName string `json:"displayName"`
HardwareDriverSettings milestoneHardwareDriverSettings `json:"hardwareDriverSettings"`
Relations milestoneRelations `json:"relations"`
}
type milestoneHardwareDriverSettings struct {
DetectedModelName string `json:"detectedModelName"`
ProductID string `json:"productID"`
MacAddress string `json:"macAddress"`
IPAddress string `json:"ipAddress"`
Address string `json:"address"`
HostName string `json:"hostName"`
DeviceModelName string `json:"deviceModelName"`
DeviceManufacturer string `json:"deviceManufacturer"`
FirmwareVersion string `json:"firmwareVersion"`
SerialNumber string `json:"serialNumber"`
}
type milestoneHardwareDevice struct {
HardwareID string
RecordingServerID string
DisplayName string
DeviceName string
Manufacturer string
MacAddress string
IPAddress string
Port string
DeviceModelName string
SerialNumber string
FirmwareVersion string
Enabled bool
Cameras []milestoneNamedDevice
Microphones []milestoneNamedDevice
}
func writeMilestoneSyncEvent(w http.ResponseWriter, event milestoneSyncEvent) {
if err := json.NewEncoder(w).Encode(event); err != nil {
log.Printf("Milestone sync stream event could not be written: %v", err)
return
}
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}
func emitMilestoneSyncEvent(
progress func(milestoneSyncEvent),
event milestoneSyncEvent,
) {
if progress != nil {
progress(event)
}
}
func (s *Server) refreshMilestoneTokenAndServerID(
ctx context.Context,
credentials milestoneCredentials,
) (milestoneSyncResult, error) {
return s.refreshMilestoneTokenAndServerIDWithProgress(ctx, credentials, nil)
}
func (s *Server) refreshMilestoneTokenAndServerIDWithProgress(
ctx context.Context,
credentials milestoneCredentials,
progress func(milestoneSyncEvent),
) (milestoneSyncResult, error) {
var syncResult milestoneSyncResult
tokenResponse, err := s.requestMilestoneToken(ctx, credentials)
if err != nil {
return syncResult, err
}
accessToken := strings.TrimSpace(tokenResponse.AccessToken)
if accessToken == "" {
return syncResult, errors.New("milestone returned empty access token")
}
tokenType := strings.TrimSpace(tokenResponse.TokenType)
if tokenType == "" {
tokenType = "Bearer"
}
expiresIn := tokenResponse.ExpiresIn
if expiresIn <= 0 {
expiresIn = 3600
}
tokenExpiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second)
syncResult.TokenReceived = true
syncResult.TokenType = tokenType
syncResult.TokenExpiresAt = tokenExpiresAt
emitMilestoneSyncEvent(progress, milestoneSyncEvent{
Type: "token",
TokenType: tokenType,
TokenExpiresAt: &tokenExpiresAt,
})
serverID, serverName, serverVersion, err := s.requestMilestoneServerID(
ctx,
credentials.Host,
tokenType,
accessToken,
credentials.SkipTLSVerify,
)
if err != nil {
return syncResult, err
}
syncResult.RecordingServerID = serverID
syncResult.RecordingServerName = serverName
syncResult.RecordingServerVersion = serverVersion
emitMilestoneSyncEvent(progress, milestoneSyncEvent{
Type: "recordingServer",
RecordingServerID: serverID,
RecordingServerName: serverName,
RecordingServerVersion: serverVersion,
})
hardwareDevices, err := s.requestMilestoneHardwareDevices(
ctx,
credentials.Host,
serverID,
accessToken,
credentials.SkipTLSVerify,
)
if err != nil {
return syncResult, err
}
tx, err := s.db.Begin(ctx)
if err != nil {
return syncResult, err
}
defer tx.Rollback(ctx)
encryptionKey := s.milestoneEncryptionKey()
_, err = tx.Exec(
ctx,
`
UPDATE milestone_settings
SET
access_token_encrypted = pgp_sym_encrypt($1, $2),
token_type = $3,
token_expires_at = $4,
token_updated_at = now(),
server_id = $5,
server_name = $6,
server_version = $7,
server_updated_at = now(),
updated_at = now()
WHERE id = 1
`,
accessToken,
encryptionKey,
tokenType,
tokenExpiresAt,
serverID,
serverName,
serverVersion,
)
if err != nil {
return syncResult, err
}
syncedHardwareCount, err := syncMilestoneHardwareDevices(ctx, tx, hardwareDevices)
if err != nil {
return syncResult, err
}
syncResult.HardwareCount = syncedHardwareCount
if err := tx.Commit(ctx); err != nil {
return syncResult, err
}
emitMilestoneSyncEvent(progress, milestoneSyncEvent{
Type: "hardware",
HardwareCount: syncResult.HardwareCount,
})
log.Printf(
"Milestone sync completed successfully: recordingServerId=%s hardwareDevices=%d",
serverID,
syncedHardwareCount,
)
return syncResult, nil
}
func (s *Server) requestMilestoneServerID(
ctx context.Context,
host string,
tokenType string,
accessToken string,
skipTLSVerify bool,
) (string, string, string, error) {
requestURL := strings.TrimRight(host, "/") + "/API/rest/v1/recordingServers"
log.Printf(
"Requesting Milestone recording server id: url=%s skipTLSVerify=%t",
requestURL,
skipTLSVerify,
)
request, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
requestURL,
nil,
)
if err != nil {
return "", "", "", err
}
if tokenType == "" {
tokenType = "Bearer"
}
request.Header.Set("Authorization", tokenType+" "+accessToken)
request.Header.Set("Accept", "application/json")
client := milestoneHTTPClient(skipTLSVerify)
response, err := client.Do(request)
if err != nil {
return "", "", "", err
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return "", "", "", err
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return "", "", "", fmt.Errorf(
"milestone recording servers request failed: url=%s status=%d body=%s",
requestURL,
response.StatusCode,
truncateMilestoneLogBody(body),
)
}
var recordingServersResponse milestoneRecordingServersResponse
if err := json.Unmarshal(body, &recordingServersResponse); err != nil {
return "", "", "", fmt.Errorf(
"milestone recording servers response could not be parsed: body=%s error=%w",
truncateMilestoneLogBody(body),
err,
)
}
if len(recordingServersResponse.Array) == 0 {
return "", "", "", fmt.Errorf(
"milestone returned no recording servers: body=%s",
truncateMilestoneLogBody(body),
)
}
recordingServer := recordingServersResponse.Array[0]
recordingServerID := strings.TrimSpace(recordingServer.ID)
recordingServerName := strings.TrimSpace(recordingServer.DisplayName)
if recordingServerName == "" {
recordingServerName = strings.TrimSpace(recordingServer.Name)
}
recordingServerVersion := strings.TrimSpace(recordingServer.Version)
if recordingServerVersion == "" {
recordingServerVersion = strings.TrimSpace(recordingServer.ProductVersion)
}
if recordingServerVersion == "" {
recordingServerVersion = strings.TrimSpace(recordingServer.RecordingServerVersion)
}
if recordingServerID == "" {
return "", "", "", fmt.Errorf(
"milestone returned recording server without id: body=%s",
truncateMilestoneLogBody(body),
)
}
log.Printf(
"Milestone recording server received successfully: id=%s displayName=%q version=%q",
recordingServerID,
recordingServerName,
recordingServerVersion,
)
return recordingServerID, recordingServerName, recordingServerVersion, nil
}
func (s *Server) requestMilestoneHardwareDevices(
ctx context.Context,
host string,
recordingServerID string,
accessToken string,
skipTLSVerify bool,
) ([]milestoneHardwareDevice, error) {
hardwareItems, err := s.requestMilestoneHardware(
ctx,
host,
recordingServerID,
accessToken,
skipTLSVerify,
)
if err != nil {
return nil, err
}
result := make([]milestoneHardwareDevice, 0, len(hardwareItems))
for _, hardware := range hardwareItems {
hardwareID := strings.TrimSpace(hardware.ID)
if hardwareID == "" {
hardwareID = strings.TrimSpace(hardware.Relations.Self.ID)
}
if hardwareID == "" {
log.Printf("Milestone hardware skipped: missing hardware id")
continue
}
settings, err := s.requestMilestoneHardwareDriverSettings(
ctx,
host,
hardwareID,
accessToken,
skipTLSVerify,
)
if err != nil {
return nil, err
}
hardwareDetails, err := s.requestMilestoneHardwareDetails(
ctx,
host,
hardwareID,
accessToken,
skipTLSVerify,
)
if err != nil {
return nil, err
}
displayName := strings.TrimSpace(hardware.DisplayName)
if displayName == "" {
displayName = strings.TrimSpace(hardware.Name)
}
model := strings.TrimSpace(settings.DeviceModelName)
if model == "" {
model = strings.TrimSpace(settings.DetectedModelName)
}
manufacturer := normalizeMilestoneManufacturer(
settings.DeviceManufacturer,
model,
)
if manufacturer == "" {
manufacturer = normalizeMilestoneManufacturer(
settings.ProductID,
model,
)
}
if manufacturer == "" {
manufacturer = extractMilestoneManufacturerFromName(model)
}
deviceName := strings.TrimSpace(hardware.DeviceName)
if deviceName == "" {
deviceName = displayName
}
enabled := true
if hardware.Enabled != nil {
enabled = *hardware.Enabled
}
if hardware.Disabled != nil {
enabled = !*hardware.Disabled
}
hardwareAddress := strings.TrimSpace(milestoneStringValue(hardwareDetails["address"]))
if hardwareAddress == "" {
hardwareAddress = hardware.Address
}
ipAddress, port := extractMilestoneHardwareAddressParts(hardwareAddress)
embeddedCameras := milestoneNamedDevicesFromHardwareCollection(
hardwareDetails,
"cameras",
)
embeddedMicrophones := milestoneNamedDevicesFromHardwareCollection(
hardwareDetails,
"microphones",
)
camerasWithDisabled, err := s.requestMilestoneNamedDevicesByHardware(
ctx,
host,
"cameras",
hardwareID,
accessToken,
skipTLSVerify,
)
if err != nil {
return nil, err
}
microphonesWithDisabled, err := s.requestMilestoneNamedDevicesByHardware(
ctx,
host,
"microphones",
hardwareID,
accessToken,
skipTLSVerify,
)
if err != nil {
return nil, err
}
cameras := mergeMilestoneNamedDevices(
embeddedCameras,
camerasWithDisabled,
)
microphones := mergeMilestoneNamedDevices(
embeddedMicrophones,
microphonesWithDisabled,
)
result = append(result, milestoneHardwareDevice{
HardwareID: hardwareID,
RecordingServerID: recordingServerID,
DisplayName: displayName,
DeviceName: deviceName,
Manufacturer: manufacturer,
MacAddress: strings.TrimSpace(settings.MacAddress),
IPAddress: ipAddress,
Port: port,
DeviceModelName: model,
SerialNumber: strings.TrimSpace(settings.SerialNumber),
FirmwareVersion: strings.TrimSpace(settings.FirmwareVersion),
Enabled: enabled,
Cameras: cameras,
Microphones: microphones,
})
}
cameraCount := 0
microphoneCount := 0
for _, hardwareDevice := range result {
cameraCount += len(hardwareDevice.Cameras)
microphoneCount += len(hardwareDevice.Microphones)
}
log.Printf(
"Milestone hardware loaded successfully: recordingServerId=%s count=%d cameras=%d microphones=%d",
recordingServerID,
len(result),
cameraCount,
microphoneCount,
)
return result, nil
}
func (s *Server) requestMilestoneNamedDevicesByHardware(
ctx context.Context,
host string,
collection string,
hardwareID string,
accessToken string,
skipTLSVerify bool,
) ([]milestoneNamedDevice, error) {
switch collection {
case "cameras", "microphones":
default:
return nil, fmt.Errorf("unsupported milestone device collection: %s", collection)
}
requestURL := strings.TrimRight(host, "/") +
"/API/rest/v1/" +
collection +
"?disabled"
log.Printf(
"Requesting Milestone %s with disabled: url=%s hardwareId=%s skipTLSVerify=%t",
collection,
requestURL,
hardwareID,
skipTLSVerify,
)
body, err := requestMilestoneJSON(
ctx,
requestURL,
accessToken,
skipTLSVerify,
)
if err != nil {
return nil, err
}
return milestoneNamedDevicesFromListResponse(body, collection, hardwareID)
}
func extractMilestoneHardwareIPAddress(value string) string {
host, _ := extractMilestoneHardwareAddressParts(value)
return host
}
func defaultMilestonePortForScheme(scheme string) string {
switch strings.ToLower(strings.TrimSpace(scheme)) {
case "https":
return "443"
case "http":
return "80"
default:
return "80"
}
}
func extractMilestoneHardwareAddressParts(value string) (string, string) {
value = strings.TrimSpace(value)
if value == "" {
return "", ""
}
parsedURL, err := url.Parse(value)
if err == nil && parsedURL.Hostname() != "" {
scheme := strings.ToLower(strings.TrimSpace(parsedURL.Scheme))
if scheme == "" {
scheme = "http"
}
port := strings.TrimSpace(parsedURL.Port())
if port == "" {
port = defaultMilestonePortForScheme(scheme)
}
return strings.TrimSpace(parsedURL.Hostname()), port
}
scheme := "http"
lowerValue := strings.ToLower(value)
if strings.HasPrefix(lowerValue, "https://") {
scheme = "https"
}
value = strings.TrimPrefix(value, "http://")
value = strings.TrimPrefix(value, "https://")
value = strings.TrimRight(value, "/")
if host, port, found := strings.Cut(value, ":"); found {
port = strings.TrimSpace(port)
if isOnlyDigits(port) {
return strings.TrimSpace(host), port
}
}
return strings.TrimSpace(value), defaultMilestonePortForScheme(scheme)
}
func extractMilestoneHardwarePort(value string) string {
_, port := extractMilestoneHardwareAddressParts(value)
return port
}
func (s *Server) requestMilestoneHardware(
ctx context.Context,
host string,
recordingServerID string,
accessToken string,
skipTLSVerify bool,
) ([]milestoneHardware, error) {
requestURL := strings.TrimRight(host, "/") +
"/API/rest/v1/recordingServers/" +
url.PathEscape(recordingServerID) +
"/hardware?disabled"
log.Printf(
"Requesting Milestone hardware: url=%s skipTLSVerify=%t",
requestURL,
skipTLSVerify,
)
body, err := requestMilestoneJSON(
ctx,
requestURL,
accessToken,
skipTLSVerify,
)
if err != nil {
return nil, err
}
var hardwareResponse milestoneHardwareResponse
if err := json.Unmarshal(body, &hardwareResponse); err != nil {
return nil, fmt.Errorf(
"milestone hardware response could not be parsed: body=%s error=%w",
truncateMilestoneLogBody(body),
err,
)
}
return hardwareResponse.Array, nil
}
func (s *Server) requestMilestoneHardwareDetails(
ctx context.Context,
host string,
hardwareID string,
accessToken string,
skipTLSVerify bool,
) (map[string]any, error) {
requestURL := strings.TrimRight(host, "/") +
"/API/rest/v1/hardware/" +
url.PathEscape(hardwareID)
log.Printf(
"Requesting Milestone hardware details: url=%s skipTLSVerify=%t",
requestURL,
skipTLSVerify,
)
body, err := requestMilestoneJSON(
ctx,
requestURL,
accessToken,
skipTLSVerify,
)
if err != nil {
return nil, err
}
var envelope milestoneHardwareUpdateEnvelope
if err := json.Unmarshal(body, &envelope); err == nil && envelope.Data != nil {
return envelope.Data, nil
}
var direct map[string]any
if err := json.Unmarshal(body, &direct); err != nil {
return nil, fmt.Errorf(
"milestone hardware details response could not be parsed: body=%s error=%w",
truncateMilestoneLogBody(body),
err,
)
}
if data, ok := direct["data"].(map[string]any); ok {
return data, nil
}
return direct, nil
}
func (s *Server) requestMilestoneHardwareDriverSettings(
ctx context.Context,
host string,
hardwareID string,
accessToken string,
skipTLSVerify bool,
) (milestoneHardwareDriverSettings, error) {
requestURL := strings.TrimRight(host, "/") +
"/API/rest/v1/hardware/" +
url.PathEscape(hardwareID) +
"/hardwareDriverSettings"
log.Printf(
"Requesting Milestone hardware driver settings: url=%s skipTLSVerify=%t",
requestURL,
skipTLSVerify,
)
body, err := requestMilestoneJSON(
ctx,
requestURL,
accessToken,
skipTLSVerify,
)
if err != nil {
return milestoneHardwareDriverSettings{}, err
}
var settingsResponse milestoneHardwareDriverSettingsResponse
if err := json.Unmarshal(body, &settingsResponse); err != nil {
return milestoneHardwareDriverSettings{}, fmt.Errorf(
"milestone hardware driver settings response could not be parsed: body=%s error=%w",
truncateMilestoneLogBody(body),
err,
)
}
if len(settingsResponse.Array) == 0 {
return milestoneHardwareDriverSettings{}, fmt.Errorf(
"milestone returned no hardware driver settings: hardwareId=%s body=%s",
hardwareID,
truncateMilestoneLogBody(body),
)
}
return settingsResponse.Array[0].HardwareDriverSettings, nil
}
func requestMilestoneJSON(
ctx context.Context,
requestURL string,
accessToken string,
skipTLSVerify bool,
) ([]byte, error) {
request, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
requestURL,
nil,
)
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+accessToken)
request.Header.Set("Accept", "application/json")
client := milestoneHTTPClient(skipTLSVerify)
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf(
"milestone request failed: url=%s status=%d body=%s",
requestURL,
response.StatusCode,
truncateMilestoneLogBody(body),
)
}
return body, nil
}
func syncMilestoneHardwareDevices(
ctx context.Context,
tx pgx.Tx,
hardwareDevices []milestoneHardwareDevice,
) (int, error) {
syncedCount := 0
for _, hardwareDevice := range hardwareDevices {
hardwareDevice.HardwareID = strings.TrimSpace(hardwareDevice.HardwareID)
hardwareDevice.RecordingServerID = strings.TrimSpace(hardwareDevice.RecordingServerID)
hardwareDevice.DisplayName = strings.TrimSpace(hardwareDevice.DisplayName)
hardwareDevice.DeviceName = strings.TrimSpace(hardwareDevice.DeviceName)
hardwareDevice.Manufacturer = strings.TrimSpace(hardwareDevice.Manufacturer)
hardwareDevice.DeviceModelName = strings.TrimSpace(hardwareDevice.DeviceModelName)
hardwareDevice.SerialNumber = strings.TrimSpace(hardwareDevice.SerialNumber)
hardwareDevice.MacAddress = strings.TrimSpace(hardwareDevice.MacAddress)
hardwareDevice.IPAddress = strings.TrimSpace(hardwareDevice.IPAddress)
hardwareDevice.Port = strings.TrimSpace(hardwareDevice.Port)
hardwareDevice.FirmwareVersion = strings.TrimSpace(hardwareDevice.FirmwareVersion)
if hardwareDevice.HardwareID == "" {
continue
}
if hardwareDevice.Manufacturer != "" {
if err := ensureOptionalDeviceLookupValue(
ctx,
tx,
"device_manufacturers",
hardwareDevice.Manufacturer,
); err != nil {
return syncedCount, err
}
}
synced, err := createOrUpdateMilestoneHardwareDevice(ctx, tx, hardwareDevice)
if err != nil {
return syncedCount, err
}
if err := syncMilestoneHardwareChildDevices(ctx, tx, hardwareDevice); err != nil {
return syncedCount, err
}
if synced {
syncedCount++
}
}
return syncedCount, nil
}
func syncMilestoneHardwareChildDevices(
ctx context.Context,
tx pgx.Tx,
hardwareDevice milestoneHardwareDevice,
) error {
hardwareID := strings.TrimSpace(hardwareDevice.HardwareID)
if hardwareID == "" {
return nil
}
_, err := tx.Exec(
ctx,
`
DELETE FROM milestone_hardware_child_devices
WHERE milestone_hardware_id = $1
`,
hardwareID,
)
if err != nil {
return err
}
for _, camera := range hardwareDevice.Cameras {
if err := upsertMilestoneHardwareChildDevice(
ctx,
tx,
hardwareID,
"camera",
camera,
); err != nil {
return err
}
}
for _, microphone := range hardwareDevice.Microphones {
if err := upsertMilestoneHardwareChildDevice(
ctx,
tx,
hardwareID,
"microphone",
microphone,
); err != nil {
return err
}
}
return nil
}
func upsertMilestoneHardwareChildDevice(
ctx context.Context,
tx pgx.Tx,
hardwareID string,
deviceType string,
childDevice milestoneNamedDevice,
) error {
milestoneDeviceID := strings.TrimSpace(childDevice.ID)
if milestoneDeviceID == "" {
return nil
}
name := strings.TrimSpace(childDevice.Name)
displayName := strings.TrimSpace(childDevice.DisplayName)
shortName := strings.TrimSpace(childDevice.ShortName)
if name == "" {
name = displayName
}
if name == "" {
name = shortName
}
if displayName == "" {
displayName = name
}
if shortName == "" {
shortName = name
}
_, err := tx.Exec(
ctx,
`
INSERT INTO milestone_hardware_child_devices (
milestone_hardware_id,
milestone_device_id,
device_type,
name,
display_name,
short_name,
enabled,
recording_enabled,
recording_storage_id
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (milestone_hardware_id, milestone_device_id, device_type)
DO UPDATE SET
name = EXCLUDED.name,
display_name = EXCLUDED.display_name,
short_name = EXCLUDED.short_name,
enabled = EXCLUDED.enabled,
recording_enabled = EXCLUDED.recording_enabled,
recording_storage_id = EXCLUDED.recording_storage_id,
updated_at = now()
`,
hardwareID,
milestoneDeviceID,
deviceType,
name,
displayName,
shortName,
childDevice.Enabled,
childDevice.RecordingEnabled,
strings.TrimSpace(childDevice.RecordingStorageID),
)
return err
}
func createOrUpdateMilestoneHardwareDevice(
ctx context.Context,
tx pgx.Tx,
hardwareDevice milestoneHardwareDevice,
) (bool, error) {
inventoryNumber, err := buildMilestoneInventoryNumber(ctx, tx, hardwareDevice)
if err != nil {
return false, err
}
commandTag, err := tx.Exec(
ctx,
`
INSERT INTO devices (
inventory_number,
manufacturer,
model,
serial_number,
mac_address,
ip_address,
device_category,
milestone_port,
location,
loan_status,
is_bao_device,
comment,
firmware_version,
milestone_recording_server_id,
milestone_hardware_id,
milestone_display_name,
milestone_enabled,
milestone_synced_at
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
'Kameras',
$7,
'',
'Verfügbar',
false,
'',
$8,
$9,
$10,
$11,
$12,
now()
)
ON CONFLICT (milestone_hardware_id)
WHERE milestone_hardware_id <> ''
DO UPDATE SET
inventory_number = EXCLUDED.inventory_number,
manufacturer = EXCLUDED.manufacturer,
model = EXCLUDED.model,
serial_number = EXCLUDED.serial_number,
mac_address = EXCLUDED.mac_address,
ip_address = EXCLUDED.ip_address,
device_category = 'Kameras',
milestone_port = EXCLUDED.milestone_port,
firmware_version = EXCLUDED.firmware_version,
milestone_recording_server_id = EXCLUDED.milestone_recording_server_id,
milestone_display_name = EXCLUDED.milestone_display_name,
milestone_enabled = EXCLUDED.milestone_enabled,
milestone_synced_at = now(),
updated_at = now()
`,
inventoryNumber,
hardwareDevice.Manufacturer,
hardwareDevice.DeviceModelName,
hardwareDevice.SerialNumber,
hardwareDevice.MacAddress,
hardwareDevice.IPAddress,
hardwareDevice.Port,
hardwareDevice.FirmwareVersion,
hardwareDevice.RecordingServerID,
hardwareDevice.HardwareID,
hardwareDevice.DisplayName,
hardwareDevice.Enabled,
)
if err != nil {
return false, err
}
return commandTag.RowsAffected() > 0, nil
}