// backend\milestone_devices.go package main import ( "context" "strings" "unicode" "github.com/jackc/pgx/v5" ) 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 } } for _, input := range hardwareDevice.Inputs { if err := upsertMilestoneHardwareChildDevice( ctx, tx, hardwareID, "input", input, ); err != nil { return err } } for _, output := range hardwareDevice.Outputs { if err := upsertMilestoneHardwareChildDevice( ctx, tx, hardwareID, "output", output, ); err != nil { return err } } for _, speaker := range hardwareDevice.Speakers { if err := upsertMilestoneHardwareChildDevice( ctx, tx, hardwareID, "speaker", speaker, ); err != nil { return err } } for _, metadata := range hardwareDevice.Metadata { if err := upsertMilestoneHardwareChildDevice( ctx, tx, hardwareID, "metadata", metadata, ); 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 ) VALUES ($1, $2, $3, $4, $5, $6, $7) 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, updated_at = now() `, hardwareID, milestoneDeviceID, deviceType, name, displayName, shortName, childDevice.Enabled, ) 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, 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, $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, 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 } func normalizeMilestoneManufacturer(value string, model string) string { value = strings.TrimSpace(value) if value == "" { return "" } model = strings.TrimSpace(model) normalizedValue := strings.ToLower(value) if strings.EqualFold(value, model) || strings.HasSuffix(normalizedValue, " device") || strings.Contains(normalizedValue, " camera") || strings.Contains(normalizedValue, " encoder") || strings.Contains(normalizedValue, " video") { if manufacturer := extractMilestoneManufacturerFromName(value); manufacturer != "" { return manufacturer } } return value } func extractMilestoneManufacturerFromName(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } parts := strings.FieldsFunc(value, func(r rune) bool { return unicode.IsSpace(r) || r == '_' || r == '-' || r == '/' || r == '\\' || r == '(' || r == ')' || r == ',' }) ignoredWords := map[string]bool{ "device": true, "camera": true, "video": true, "encoder": true, "ip": true, "network": true, "hardware": true, } for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } if isOnlyDigits(part) { continue } if ignoredWords[strings.ToLower(part)] { continue } return part } return "" } func buildMilestoneInventoryNumber( ctx context.Context, tx pgx.Tx, hardwareDevice milestoneHardwareDevice, ) (string, error) { inventoryNumber := extractInventoryNumberFromMilestoneDeviceName( hardwareDevice.DeviceName, ) if inventoryNumber == "" { inventoryNumber = extractInventoryNumberFromMilestoneDeviceName( hardwareDevice.DisplayName, ) } if inventoryNumber == "" { inventoryNumber = buildGeneratedMilestoneInventoryNumber( hardwareDevice.HardwareID, ) } var exists bool err := tx.QueryRow( ctx, ` SELECT EXISTS( SELECT 1 FROM devices WHERE lower(inventory_number) = lower($1) AND milestone_hardware_id <> $2 ) `, inventoryNumber, hardwareDevice.HardwareID, ).Scan(&exists) if err != nil { return "", err } if !exists { return inventoryNumber, nil } suffix := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(hardwareDevice.HardwareID), "-", "")) if len(suffix) > 8 { suffix = suffix[:8] } if suffix == "" { suffix = "MS" } return inventoryNumber + "-" + suffix, nil } func extractInventoryNumberFromMilestoneDeviceName(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } parts := strings.FieldsFunc(value, func(r rune) bool { return r == '_' || r == '-' || unicode.IsSpace(r) }) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } if isOnlyDigits(part) { return part } } return "" } func isOnlyDigits(value string) bool { if value == "" { return false } for _, r := range value { if r < '0' || r > '9' { return false } } return true } func buildGeneratedMilestoneInventoryNumber(hardwareID string) string { value := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(hardwareID), "-", "")) if len(value) > 12 { value = value[:12] } if value == "" { value = "UNKNOWN" } return "MS-" + value }