// Package dell provides parser for Dell TSR archives. package dell import ( "archive/zip" "bytes" "encoding/json" "encoding/xml" "fmt" "io" "regexp" "sort" "strconv" "strings" "time" "git.mchus.pro/mchus/logpile/internal/models" "git.mchus.pro/mchus/logpile/internal/parser" "git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids" ) const parserVersion = "3.0" func init() { parser.Register(&Parser{}) } // Parser implements VendorParser for Dell TSR archives. type Parser struct{} func (p *Parser) Name() string { return "Dell TSR Parser" } func (p *Parser) Vendor() string { return "dell" } func (p *Parser) Version() string { return parserVersion } func (p *Parser) Detect(files []parser.ExtractedFile) int { confidence := 0 expanded := expandNestedZipFiles(files) for _, f := range expanded { path := strings.ToLower(strings.TrimSpace(f.Path)) switch { case strings.HasSuffix(path, ".pl.zip"): confidence += 40 case strings.HasSuffix(path, "tsr/metadata.json") || strings.HasSuffix(path, "/metadata.json"): confidence += 25 case strings.HasSuffix(path, "sysinfo_dcim_view.xml"): confidence += 30 case strings.HasSuffix(path, "sysinfo_dcim_softwareidentity.xml"): confidence += 20 case strings.HasSuffix(path, "curr_lclog.xml"): confidence += 10 case path == "signature": confidence += 5 } if confidence >= 100 { return 100 } } return confidence } func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) { expanded := expandNestedZipFiles(files) result := &models.AnalysisResult{ Events: make([]models.Event, 0), FRU: make([]models.FRUInfo, 0), Sensors: make([]models.SensorReading, 0), Hardware: &models.HardwareConfig{ Firmware: make([]models.FirmwareInfo, 0), CPUs: make([]models.CPU, 0), Memory: make([]models.MemoryDIMM, 0), Storage: make([]models.Storage, 0), Volumes: make([]models.StorageVolume, 0), PCIeDevices: make([]models.PCIeDevice, 0), GPUs: make([]models.GPU, 0), NetworkAdapters: make([]models.NetworkAdapter, 0), NetworkCards: make([]models.NIC, 0), PowerSupply: make([]models.PSU, 0), }, } if f := findBySuffix(expanded, "tsr/metadata.json", "/metadata.json"); f != nil { parseMetadataJSON(f.Content, result) } if f := findBySuffix(expanded, "sysinfo_dcim_view.xml"); f != nil { parseDCIMViewXML(f.Content, result) } if f := findBySuffix(expanded, "sysinfo_dcim_softwareidentity.xml"); f != nil { parseSoftwareIdentityXML(f.Content, result) } if f := findBySuffix(expanded, "sysinfo_cim_sensor.xml"); f != nil { parseCIMSensorXML(f.Content, result) } if f := findBySuffix(expanded, "curr_lclog.xml"); f != nil { result.Events = append(result.Events, parseLCEventsXML(f.Content)...) } result.Hardware.Storage = dedupeStorage(result.Hardware.Storage) result.Hardware.Volumes = dedupeVolumes(result.Hardware.Volumes) result.Hardware.PowerSupply = dedupePSU(result.Hardware.PowerSupply) result.Hardware.NetworkAdapters = dedupeNetworkAdapters(result.Hardware.NetworkAdapters) result.Hardware.NetworkCards = nicCardsFromAdapters(result.Hardware.NetworkAdapters) result.Hardware.GPUs = dedupeGPU(result.Hardware.GPUs) result.Hardware.PCIeDevices = removePCIeOverlappingWithGPUs(dedupePCIe(result.Hardware.PCIeDevices), result.Hardware.GPUs) result.Hardware.CPUs = dedupeCPU(result.Hardware.CPUs) result.Hardware.Memory = dedupeDIMM(result.Hardware.Memory) result.Hardware.Firmware = dedupeFirmware(result.Hardware.Firmware) result.Sensors = dedupeSensors(result.Sensors) return result, nil } type metadataPayload struct { Make string `json:"Make"` Model string `json:"Model"` ServiceTag string `json:"ServiceTag"` FirmwareVersion string `json:"FirmwareVersion"` CollectionDateTime string `json:"CollectionDateTime"` } func parseMetadataJSON(content []byte, result *models.AnalysisResult) { var m metadataPayload if err := json.Unmarshal(content, &m); err != nil { return } if result.Hardware == nil { return } setIfEmpty(&result.Hardware.BoardInfo.Manufacturer, strings.TrimSpace(m.Make)) setIfEmpty(&result.Hardware.BoardInfo.ProductName, strings.TrimSpace(m.Model)) setIfEmpty(&result.Hardware.BoardInfo.SerialNumber, strings.TrimSpace(m.ServiceTag)) fw := strings.TrimSpace(m.FirmwareVersion) if fw != "" { result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{ DeviceName: "iDRAC", Version: fw, }) } if t := parseDellTSRTime(m.CollectionDateTime); !t.IsZero() { result.CollectedAt = t.UTC() } } type cimDoc struct { XMLName xml.Name `xml:"CIM"` Message struct { SimpleReq struct { Named []cimNamedInstance `xml:"VALUE.NAMEDINSTANCE"` } `xml:"SIMPLEREQ"` } `xml:"MESSAGE"` } type cimNamedInstance struct { Instance cimInstance `xml:"INSTANCE"` } type cimInstance struct { ClassName string `xml:"CLASSNAME,attr"` Props []cimProperty `xml:",any"` } type cimProperty struct { XMLName xml.Name Name string `xml:"NAME,attr"` Value string `xml:"VALUE"` DisplayValue string `xml:"DisplayValue"` ArrayValues []string `xml:"VALUE.ARRAY>VALUE"` ArrayDisplay []string `xml:"VALUE.ARRAY>DisplayValue"` } func parseDCIMViewXML(content []byte, result *models.AnalysisResult) { var doc cimDoc if err := xml.Unmarshal(content, &doc); err != nil { return } for _, ni := range doc.Message.SimpleReq.Named { inst := ni.Instance if strings.TrimSpace(inst.ClassName) == "" { continue } props := propertyMap(inst.Props) switch inst.ClassName { case "DCIM_SystemView": parseSystemView(props, result) case "DCIM_CPUView": parseCPUView(props, result) case "DCIM_MemoryView": parseMemoryView(props, result) case "DCIM_PhysicalDiskView": parsePhysicalDiskView(props, result) case "DCIM_VirtualDiskView": parseVirtualDiskView(props, result) case "DCIM_PowerSupplyView": parsePowerSupplyView(props, result) case "DCIM_PCIDeviceView": parsePCIeDeviceView(props, result) case "DCIM_NICView", "DCIM_InfiniBandView": parseNICView(props, result) case "DCIM_VideoView": parseVideoView(props, result) case "DCIM_ControllerView": parseControllerView(props, result) case "DCIM_ControllerBatteryView": parseControllerBatteryView(props, result) case "DCIM_EnclosureView": parseEnclosureView(props, result) case "DCIM_iDRACCardView": parseIDRACCardView(props, result) case "DCIM_FanView": parseFanView(props, result) } } } func parseCIMSensorXML(content []byte, result *models.AnalysisResult) { var doc cimDoc if err := xml.Unmarshal(content, &doc); err != nil { return } for _, ni := range doc.Message.SimpleReq.Named { inst := ni.Instance props := propertyMap(inst.Props) switch inst.ClassName { case "DCIM_GPUSensor": parseGPUCIMSensor(props, result) default: parseGenericCIMSensor(props, result) } } } func parseSoftwareIdentityXML(content []byte, result *models.AnalysisResult) { var doc cimDoc if err := xml.Unmarshal(content, &doc); err != nil { return } for _, ni := range doc.Message.SimpleReq.Named { props := propertyMap(ni.Instance.Props) version := firstNonEmpty(props["versionstring"], props["revisionstring"]) if strings.TrimSpace(version) == "" { continue } name := nicMACInModelRE.ReplaceAllString(firstNonEmpty(props["elementname"], props["fqdd"], props["instanceid"]), "") if strings.TrimSpace(name) == "" { continue } result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{ DeviceName: strings.TrimSpace(name), Version: strings.TrimSpace(version), Description: strings.TrimSpace(firstNonEmpty(props["fqdd"], props["componenttype"])), }) } } type lcEvents struct { Events []lcEvent `xml:"Event"` } type lcEvent struct { AgentID string `xml:"AgentID,attr"` Category string `xml:"Category,attr"` Severity string `xml:"Severity,attr"` Timestamp string `xml:"Timestamp,attr"` Message string `xml:"Message"` MessageID string `xml:"MessageID"` FQDD string `xml:"FQDD"` } func parseLCEventsXML(content []byte) []models.Event { var doc lcEvents if err := xml.Unmarshal(content, &doc); err != nil { return nil } out := make([]models.Event, 0, len(doc.Events)) for _, e := range doc.Events { ts := parseDellEventTime(e.Timestamp) desc := strings.TrimSpace(e.Message) if desc == "" { continue } out = append(out, models.Event{ ID: strings.TrimSpace(e.MessageID), Timestamp: ts, Source: firstNonEmpty(strings.TrimSpace(e.AgentID), "iDRAC"), SensorName: strings.TrimSpace(e.FQDD), EventType: firstNonEmpty(strings.TrimSpace(e.Category), strings.TrimSpace(e.MessageID)), Severity: mapDellSeverity(e.Severity), Description: desc, }) } return out } func parseSystemView(props map[string]string, result *models.AnalysisResult) { board := &result.Hardware.BoardInfo setIfEmpty(&board.Manufacturer, props["manufacturer"]) setIfEmpty(&board.ProductName, props["model"]) setIfEmpty(&board.SerialNumber, props["servicetag"]) setIfEmpty(&board.PartNumber, props["boardpartnumber"]) setIfEmpty(&board.Version, props["biosversionstring"]) setIfEmpty(&board.UUID, props["uuid"]) if strings.TrimSpace(board.Description) == "" { board.Description = strings.TrimSpace(props["systemgeneration"]) } addFirmware(result, "BIOS", props["biosversionstring"], "system bios") addFirmware(result, "Lifecycle Controller", props["lifecyclecontrollerversion"], "idrac lifecycle") addFirmware(result, "CPLD", props["cpldversion"], "system cpld") } func parseCPUView(props map[string]string, result *models.AnalysisResult) { model := strings.TrimSpace(props["model"]) if model == "" { return } cpu := models.CPU{ Socket: parseSocketFromFQDD(firstNonEmpty(props["fqdd"], props["instanceid"])), Model: model, Description: strings.TrimSpace(props["manufacturer"]), Cores: parseIntLoose(firstNonEmpty(props["numberofenabledcores"], props["numberofprocessorcores"])), Threads: parseIntLoose(props["numberofenabledthreads"]), FrequencyMHz: parseIntLoose(props["currentclockspeed"]), MaxFreqMHz: parseIntLoose(props["maxclockspeed"]), PPIN: strings.TrimSpace(props["ppin"]), Status: normalizeStatus(props["primarystatus"]), } result.Hardware.CPUs = append(result.Hardware.CPUs, cpu) } func parseMemoryView(props map[string]string, result *models.AnalysisResult) { slot := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["fqdd"], props["instanceid"])) if slot == "" { return } status := normalizeStatus(props["primarystatus"]) dimm := models.MemoryDIMM{ Slot: slot, Location: strings.TrimSpace(firstNonEmpty(props["fqdd"], slot)), Present: status != "absent", SizeMB: parseIntLoose(props["size"]), Type: firstNonEmpty(props["memorytypeextended"], props["memorytype"], "DIMM"), Technology: strings.TrimSpace(props["memorytechnology"]), MaxSpeedMHz: parseIntLoose(props["speed"]), CurrentSpeedMHz: parseIntLoose(props["currentoperatingspeed"]), Manufacturer: strings.TrimSpace(props["manufacturer"]), SerialNumber: strings.TrimSpace(props["serialnumber"]), PartNumber: strings.TrimSpace(props["partnumber"]), Status: status, } result.Hardware.Memory = append(result.Hardware.Memory, dimm) } func parsePhysicalDiskView(props map[string]string, result *models.AnalysisResult) { slot := firstNonEmpty(props["slot"], extractDiskSlotFromFQDD(props["fqdd"]), props["fqdd"]) model := strings.TrimSpace(props["model"]) if strings.TrimSpace(slot) == "" && model == "" { return } st := models.Storage{ Slot: strings.TrimSpace(slot), Type: inferStorageType(props["mediatype"], props["busprotocol"]), Model: model, SizeGB: parseBytesToGB(props["sizeinbytes"]), SerialNumber: strings.TrimSpace(props["serialnumber"]), Manufacturer: strings.TrimSpace(props["manufacturer"]), Firmware: strings.TrimSpace(firstNonEmpty(props["revision"], props["firmwareversion"])), Interface: strings.TrimSpace(props["busprotocol"]), Present: true, Location: strings.TrimSpace(props["devicedescription"]), Status: normalizeStatus(firstNonEmpty(props["raidstatus"], props["primarystatus"])), } if v := strings.TrimSpace(props["remainingratedwriteendurance"]); v != "" { n := parseIntLoose(v) st.RemainingEndurancePct = &n } result.Hardware.Storage = append(result.Hardware.Storage, st) } func parseVirtualDiskView(props map[string]string, result *models.AnalysisResult) { id := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])) if id == "" { return } capacityBytes := parseInt64Loose(props["sizeinbytes"]) sizeGB := 0 if capacityBytes > 0 { sizeGB = int(capacityBytes / 1_000_000_000) } v := models.StorageVolume{ ID: id, Name: strings.TrimSpace(firstNonEmpty(props["name"], id)), Controller: extractControllerFromVolumeFQDD(id), RAIDLevel: normalizeRAIDLevel(firstNonEmpty(props["raidtypes"], props["raidstatus"])), SizeGB: sizeGB, CapacityBytes: capacityBytes, Status: normalizeStatus(firstNonEmpty(props["raidstatus"], props["primarystatus"])), } result.Hardware.Volumes = append(result.Hardware.Volumes, v) } func parsePowerSupplyView(props map[string]string, result *models.AnalysisResult) { fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])) model := strings.TrimSpace(props["model"]) if fqdd == "" && model == "" { return } slot := normalizePSUSlot(fqdd) status := normalizeStatus(props["primarystatus"]) psu := models.PSU{ Slot: slot, Present: status != "absent", Model: model, Description: strings.TrimSpace(props["devicedescription"]), Vendor: strings.TrimSpace(props["manufacturer"]), WattageW: parseIntLoose(firstNonEmpty(props["totaloutputpower"], props["effectivecapacity"])), SerialNumber: strings.TrimSpace(props["serialnumber"]), PartNumber: strings.TrimSpace(props["partnumber"]), Firmware: strings.TrimSpace(props["firmwareversion"]), Status: status, InputVoltage: parseFloatLoose(props["inputvoltage"]), InputType: strings.TrimSpace(props["type"]), } result.Hardware.PowerSupply = append(result.Hardware.PowerSupply, psu) } // pcieFQDDNoisePrefix lists FQDD prefixes that represent internal chipset/CPU // components or devices already captured with richer data elsewhere: // - HostBridge/P2PBridge/ISABridge/SMBus: AMD EPYC internal fabric, not PCIe slots // - AHCI.Embedded: AMD FCH SATA, not a slot device // - Video.Embedded: BMC/iDRAC Matrox graphics chip, not user-visible // - NIC.Embedded: already parsed from DCIM_NICView with model and MAC addresses var pcieFQDDNoisePrefix = []string{ "HostBridge.Embedded.", "P2PBridge.Embedded.", "ISABridge.Embedded.", "SMBus.Embedded.", "AHCI.Embedded.", "Video.Embedded.", // All NIC FQDD classes are parsed from DCIM_NICView / DCIM_InfiniBandView into // NetworkAdapters with model, MAC, firmware, and VendorID/DeviceID. The // DCIM_PCIDeviceView duplicate carries only DataBusWidth ("Unknown", "16x or x16") // and no useful extra data, so suppress it here. "NIC.", "InfiniBand.", } func parsePCIeDeviceView(props map[string]string, result *models.AnalysisResult) { // "description" is the chip/device model (e.g. "MT28908 Family [ConnectX-6]"); prefer // it over "devicedescription" which is the location string ("InfiniBand in Slot 1 Port 1"). desc := strings.TrimSpace(firstNonEmpty(props["description"], props["devicedescription"])) fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])) if desc == "" && fqdd == "" { return } for _, prefix := range pcieFQDDNoisePrefix { if strings.HasPrefix(fqdd, prefix) { return } } vendorID := parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["vendorid"])) deviceID := parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["deviceid"])) manufacturer := strings.TrimSpace(props["manufacturer"]) // General rule: if chip model not found in logs but PCI IDs are known, resolve from pci.ids if desc == "" && vendorID != 0 && deviceID != 0 { desc = pciids.DeviceName(vendorID, deviceID) } if manufacturer == "" && vendorID != 0 { manufacturer = pciids.VendorName(vendorID) } p := models.PCIeDevice{ Slot: fqdd, Description: desc, VendorID: vendorID, DeviceID: deviceID, BDF: formatBDF(props["busnumber"], props["devicenumber"], props["functionnumber"]), Manufacturer: manufacturer, NUMANode: parseIntLoose(props["cpuaffinity"]), Status: normalizeStatus(props["primarystatus"]), } result.Hardware.PCIeDevices = append(result.Hardware.PCIeDevices, p) } func parseNICView(props map[string]string, result *models.AnalysisResult) { fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])) model := nicMACInModelRE.ReplaceAllString(strings.TrimSpace(firstNonEmpty(props["productname"], props["devicedescription"])), "") if fqdd == "" && model == "" { return } mac := strings.TrimSpace(firstNonEmpty(props["currentmacaddress"], props["permanentmacaddress"])) vendorID := parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["vendorid"])) deviceID := parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["deviceid"])) vendor := strings.TrimSpace(firstNonEmpty(props["vendorname"], props["manufacturer"])) // Prefer pci.ids chip model over generic ProductName when PCI IDs are available. // Dell TSR often reports a marketing name (e.g. "Mellanox Network Adapter") while // pci.ids has the precise chip identifier (e.g. "MT28908 Family [ConnectX-6]"). if vendorID != 0 && deviceID != 0 { if chipModel := pciids.DeviceName(vendorID, deviceID); chipModel != "" { model = chipModel } if vendor == "" { vendor = pciids.VendorName(vendorID) } } n := models.NetworkAdapter{ Slot: fqdd, Location: strings.TrimSpace(firstNonEmpty(props["devicedescription"], fqdd)), Present: true, Model: model, Description: strings.TrimSpace(props["protocol"]), Vendor: vendor, VendorID: vendorID, DeviceID: deviceID, SerialNumber: strings.TrimSpace(props["serialnumber"]), PartNumber: strings.TrimSpace(props["partnumber"]), Firmware: strings.TrimSpace(firstNonEmpty( props["familyversion"], props["efiversion"], props["landriverversion"], props["controllerbiosversion"], )), PortCount: inferPortCountFromFQDD(fqdd), NUMANode: parseIntLoose(props["cpuaffinity"]), Status: normalizeStatus(props["primarystatus"]), } if mac != "" { n.MACAddresses = []string{mac} if n.PortCount == 0 { n.PortCount = 1 } } result.Hardware.NetworkAdapters = append(result.Hardware.NetworkAdapters, n) } func parseVideoView(props map[string]string, result *models.AnalysisResult) { fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])) model := strings.TrimSpace(firstNonEmpty(props["marketingname"], props["description"], props["devicedescription"])) serial := strings.TrimSpace(props["serialnumber"]) if fqdd == "" && model == "" && serial == "" { return } gpu := models.GPU{ Slot: fqdd, Location: strings.TrimSpace(firstNonEmpty(props["devicedescription"], fqdd)), Model: model, Description: strings.TrimSpace(props["description"]), Manufacturer: strings.TrimSpace(props["manufacturer"]), VendorID: parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["pcisubvendorid"])), DeviceID: parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["pcisubdeviceid"])), BDF: formatBDF(props["busnumber"], props["devicenumber"], props["functionnumber"]), UUID: strings.TrimSpace(props["gpuguid"]), SerialNumber: serial, PartNumber: strings.TrimSpace(firstNonEmpty(props["gpupartnumber"], props["boardpartnumber"])), Firmware: strings.TrimSpace(props["firmwareversion"]), Status: normalizeStatus(firstNonEmpty(props["gpuhealth"], props["gpustate"], props["primarystatus"])), } result.Hardware.GPUs = append(result.Hardware.GPUs, gpu) } func parseControllerView(props map[string]string, result *models.AnalysisResult) { fqdd := strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])) name := strings.TrimSpace(firstNonEmpty(props["productname"], props["devicedescription"])) if fqdd == "" && name == "" { return } result.Hardware.PCIeDevices = append(result.Hardware.PCIeDevices, models.PCIeDevice{ Slot: fqdd, Description: name, VendorID: parseHexOrDec(firstNonEmpty(props["pcivendorid"], props["pcisubvendorid"])), DeviceID: parseHexOrDec(firstNonEmpty(props["pcideviceid"], props["pcisubdeviceid"])), BDF: formatBDF(props["bus"], props["device"], props["function"]), DeviceClass: "storage-controller", Manufacturer: strings.TrimSpace(firstNonEmpty(props["devicecardmanufacturer"], props["manufacturer"])), PartNumber: strings.TrimSpace(firstNonEmpty(props["ppid"], props["boardpartnumber"])), NUMANode: parseIntLoose(props["cpuaffinity"]), Status: normalizeStatus(props["primarystatus"]), }) addFirmware(result, firstNonEmpty(name, fqdd), props["controllerfirmwareversion"], firstNonEmpty(fqdd, "storage controller")) } func parseControllerBatteryView(props map[string]string, result *models.AnalysisResult) { name := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["fqdd"], props["instanceid"])) if name == "" { return } result.Sensors = append(result.Sensors, models.SensorReading{ Name: name, Type: "battery", RawValue: strings.TrimSpace(firstNonEmpty(props["raidstate"], props["primarystatus"])), Status: normalizeStatus(firstNonEmpty(props["primarystatus"], props["raidstate"])), }) } func parseEnclosureView(props map[string]string, result *models.AnalysisResult) { desc := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["productname"])) if desc == "" { return } result.FRU = append(result.FRU, models.FRUInfo{ DeviceID: strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])), Description: desc, ProductName: strings.TrimSpace(props["productname"]), SerialNumber: strings.TrimSpace(props["servicetag"]), AssetTag: strings.TrimSpace(props["assettag"]), Version: strings.TrimSpace(props["version"]), }) } func parseIDRACCardView(props map[string]string, result *models.AnalysisResult) { addFirmware(result, "iDRAC", props["firmwareversion"], "idrac card") desc := strings.TrimSpace(firstNonEmpty(props["devicedescription"], "iDRAC")) result.FRU = append(result.FRU, models.FRUInfo{ DeviceID: strings.TrimSpace(firstNonEmpty(props["fqdd"], props["instanceid"])), Description: desc, Manufacturer: strings.TrimSpace(result.Hardware.BoardInfo.Manufacturer), ProductName: strings.TrimSpace(props["model"]), SerialNumber: strings.TrimSpace(props["guid"]), Version: strings.TrimSpace(props["firmwareversion"]), }) } func parseFanView(props map[string]string, result *models.AnalysisResult) { name := strings.TrimSpace(firstNonEmpty(props["devicedescription"], props["fqdd"], props["instanceid"])) if name == "" { return } reading := parseScaledFloat(props["currentreading"], props["unitmodifier"]) result.Sensors = append(result.Sensors, models.SensorReading{ Name: name, Type: "fan_speed", Value: reading, Unit: "RPM", RawValue: strings.TrimSpace(props["currentreading"]), Status: normalizeStatus(props["primarystatus"]), }) } func parseGPUCIMSensor(props map[string]string, result *models.AnalysisResult) { deviceID := strings.TrimSpace(props["deviceid"]) primaryTemp := parseScaledFloat(props["primarygputemperature"], "-1") memTemp := parseScaledFloat(props["memorytemperature"], "-1") power := parseIntLoose(props["powerconsumption"]) if primaryTemp > 0 { result.Sensors = append(result.Sensors, models.SensorReading{ Name: firstNonEmpty(deviceID, "GPU"), Type: "temperature", Value: primaryTemp, Unit: "C", RawValue: strings.TrimSpace(props["primarygputemperature"]), Status: normalizeCIMHealthStatus(props["thermalalertstatus"]), }) } if memTemp > 0 { result.Sensors = append(result.Sensors, models.SensorReading{ Name: firstNonEmpty(deviceID, "GPU") + " memory", Type: "temperature", Value: memTemp, Unit: "C", RawValue: strings.TrimSpace(props["memorytemperature"]), Status: normalizeCIMHealthStatus(props["thermalalertstatus"]), }) } for i := range result.Hardware.GPUs { g := &result.Hardware.GPUs[i] if deviceID != "" && !strings.EqualFold(strings.TrimSpace(g.Slot), deviceID) { continue } if g.Temperature == 0 && primaryTemp > 0 { g.Temperature = int(primaryTemp) } if g.MemTemperature == 0 && memTemp > 0 { g.MemTemperature = int(memTemp) } if g.Power == 0 && power > 0 { g.Power = power } } } func parseGenericCIMSensor(props map[string]string, result *models.AnalysisResult) { name := strings.TrimSpace(firstNonEmpty(props["elementname"], props["devicedescription"], props["deviceid"])) if name == "" { return } currentRaw := strings.TrimSpace(props["currentreading"]) if currentRaw == "" { return } value := parseScaledFloat(currentRaw, props["unitmodifier"]) unit, sensorType := mapCIMSensorUnitAndType(props["baseunits"], props["sensortype"], name) status := normalizeCIMHealthStatus(firstNonEmpty(props["primarystatus"], props["healthstate"])) result.Sensors = append(result.Sensors, models.SensorReading{ Name: name, Type: sensorType, Value: value, Unit: unit, RawValue: currentRaw, Status: status, }) } func propertyMap(props []cimProperty) map[string]string { out := make(map[string]string, len(props)) for _, p := range props { name := normalizeKey(p.Name) if name == "" { continue } value := strings.TrimSpace(p.DisplayValue) if value == "" { value = strings.TrimSpace(p.Value) } if value == "" && len(p.ArrayDisplay) > 0 { value = strings.Join(nonEmptyStrings(p.ArrayDisplay), ", ") } if value == "" && len(p.ArrayValues) > 0 { value = strings.Join(nonEmptyStrings(p.ArrayValues), ", ") } out[name] = strings.TrimSpace(value) } return out } func findBySuffix(files []parser.ExtractedFile, suffixes ...string) *parser.ExtractedFile { for i := range files { path := strings.ToLower(strings.TrimSpace(files[i].Path)) for _, suffix := range suffixes { if strings.HasSuffix(path, strings.ToLower(suffix)) { return &files[i] } } } return nil } func expandNestedZipFiles(files []parser.ExtractedFile) []parser.ExtractedFile { out := make([]parser.ExtractedFile, 0, len(files)+32) out = append(out, files...) for _, f := range files { if !strings.HasSuffix(strings.ToLower(strings.TrimSpace(f.Path)), ".zip") { continue } zr, err := zip.NewReader(bytes.NewReader(f.Content), int64(len(f.Content))) if err != nil { continue } for _, zf := range zr.File { if zf.FileInfo().IsDir() { continue } if zf.FileInfo().Size() > 10*1024*1024 { continue } rc, err := zf.Open() if err != nil { continue } content, err := io.ReadAll(rc) rc.Close() if err != nil { continue } out = append(out, parser.ExtractedFile{ Path: zf.Name, Content: content, ModTime: zf.Modified, }) } } return out } func addFirmware(result *models.AnalysisResult, name, version, desc string) { name = strings.TrimSpace(name) version = strings.TrimSpace(version) if name == "" || version == "" { return } result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{ DeviceName: name, Version: version, Description: strings.TrimSpace(desc), }) } func parseDellTSRTime(v string) time.Time { v = strings.TrimSpace(v) if v == "" { return time.Time{} } layouts := []string{ "2006-01-02 15:04:05.000-0700", "2006-01-02 15:04:05-0700", } for _, layout := range layouts { if t, err := time.Parse(layout, v); err == nil { return t } } return time.Time{} } func parseDellEventTime(v string) time.Time { v = strings.TrimSpace(v) if v == "" { return time.Time{} } layouts := []string{ "2006-01-02T15:04:05-0700", time.RFC3339, } for _, layout := range layouts { if t, err := time.Parse(layout, v); err == nil { return t } } return time.Time{} } func mapDellSeverity(v string) models.Severity { switch strings.ToLower(strings.TrimSpace(v)) { case "critical": return models.SeverityCritical case "warning": return models.SeverityWarning default: return models.SeverityInfo } } func normalizeStatus(v string) string { s := strings.ToLower(strings.TrimSpace(v)) switch { case s == "", s == "unknown", s == "not applicable": return "" case strings.Contains(s, "ok"), strings.Contains(s, "online"), strings.Contains(s, "enabled"), strings.Contains(s, "presence"): return "ok" case strings.Contains(s, "warn"), strings.Contains(s, "degrad"): return "warning" case strings.Contains(s, "critical"), strings.Contains(s, "fail"), strings.Contains(s, "error"): return "critical" case strings.Contains(s, "absent"), strings.Contains(s, "missing"): return "absent" default: return s } } func inferStorageType(mediaType, protocol string) string { s := strings.ToLower(strings.TrimSpace(mediaType + " " + protocol)) switch { case strings.Contains(s, "nvme"): return "nvme" case strings.Contains(s, "solid state"), strings.Contains(s, "ssd"): return "ssd" case strings.Contains(s, "hard disk"), strings.Contains(s, "hdd"), strings.Contains(s, "sata"), strings.Contains(s, "sas"): return "disk" default: return "disk" } } func normalizeRAIDLevel(v string) string { s := strings.TrimSpace(v) if s == "" { return "" } u := strings.ToUpper(s) if strings.HasPrefix(u, "RAID") { return u } if strings.HasPrefix(strings.ToLower(s), "raid") { return strings.ToUpper(s) } if strings.Contains(u, "RAID") { return strings.ToUpper(s) } return s } func parseSocketFromFQDD(v string) int { re := regexp.MustCompile(`(?i)socket\.(\d+)`) m := re.FindStringSubmatch(strings.TrimSpace(v)) if len(m) != 2 { return 0 } return parseIntLoose(m[1]) } func extractDiskSlotFromFQDD(v string) string { re := regexp.MustCompile(`(?i)disk\.bay\.(\d+)`) m := re.FindStringSubmatch(strings.TrimSpace(v)) if len(m) == 2 { return m[1] } return "" } func extractControllerFromVolumeFQDD(v string) string { parts := strings.Split(strings.TrimSpace(v), ":") if len(parts) < 2 { return "" } return strings.TrimSpace(parts[1]) } func normalizePSUSlot(v string) string { re := regexp.MustCompile(`(?i)psu\.slot\.(\d+)`) m := re.FindStringSubmatch(strings.TrimSpace(v)) if len(m) == 2 { return "PSU" + m[1] } return strings.TrimSpace(v) } func inferPortCountFromFQDD(v string) int { re := regexp.MustCompile(`(?i)-(\d+)-`) m := re.FindStringSubmatch(v) if len(m) == 2 { return parseIntLoose(m[1]) } return 0 } func formatBDF(bus, dev, fn string) string { b := parseIntLoose(bus) d := parseIntLoose(dev) f := parseIntLoose(fn) if b == 0 && d == 0 && f == 0 { return "" } return fmt.Sprintf("%02x:%02x.%x", b, d, f) } func parseBytesToGB(v string) int { n := parseInt64Loose(v) if n <= 0 { return 0 } return int(n / 1_000_000_000) } var firstNumberRE = regexp.MustCompile(`[-+]?[0-9]+`) var firstFloatRE = regexp.MustCompile(`[-+]?[0-9]+(?:\.[0-9]+)?`) var nicMACInModelRE = regexp.MustCompile(`\s+-\s+([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$`) func parseIntLoose(v string) int { v = strings.TrimSpace(v) if v == "" { return 0 } if i := parseHexOrDec(v); i != 0 { return i } num := firstNumberRE.FindString(v) if num == "" { return 0 } n, _ := strconv.Atoi(num) return n } func parseInt64Loose(v string) int64 { v = strings.TrimSpace(v) if v == "" { return 0 } num := firstNumberRE.FindString(v) if num == "" { return 0 } n, _ := strconv.ParseInt(num, 10, 64) return n } func parseFloatLoose(v string) float64 { v = strings.TrimSpace(v) if v == "" { return 0 } num := firstFloatRE.FindString(v) if num == "" { return 0 } f, _ := strconv.ParseFloat(num, 64) return f } func parseScaledFloat(valueRaw, unitModRaw string) float64 { base := parseFloatLoose(valueRaw) if base == 0 { return 0 } mod := parseIntLoose(unitModRaw) if mod == 0 { return base } scale := 1.0 if mod > 0 { for i := 0; i < mod; i++ { scale *= 10 } return base * scale } for i := 0; i < -mod; i++ { scale *= 10 } return base / scale } func parseHexOrDec(v string) int { v = strings.TrimSpace(strings.ToLower(v)) if v == "" { return 0 } if strings.HasPrefix(v, "0x") { n, _ := strconv.ParseInt(strings.TrimPrefix(v, "0x"), 16, 32) return int(n) } if n, err := strconv.ParseInt(v, 10, 32); err == nil { return int(n) } if n, err := strconv.ParseInt(v, 16, 32); err == nil { return int(n) } return 0 } func normalizeKey(s string) string { s = strings.ToLower(strings.TrimSpace(s)) var b strings.Builder b.Grow(len(s)) for _, r := range s { if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') { b.WriteRune(r) } } return b.String() } func mapCIMSensorUnitAndType(baseUnits, sensorTypeRaw, name string) (string, string) { switch parseIntLoose(baseUnits) { case 2: return "C", "temperature" case 5: return "V", "voltage" case 6: return "A", "current" case 7: return "W", "power" case 19: return "RPM", "fan_speed" case 65: return "%", "utilization" } lower := strings.ToLower(name) switch { case strings.Contains(lower, "temp"): return "C", "temperature" case strings.Contains(lower, "fan"): return "RPM", "fan_speed" case strings.Contains(lower, "volt"): return "V", "voltage" case strings.Contains(lower, "current"): return "A", "current" case strings.Contains(lower, "power"), strings.Contains(lower, "pwr"): return "W", "power" } switch parseIntLoose(sensorTypeRaw) { case 2: return "C", "temperature" case 3: return "V", "voltage" case 5: return "RPM", "fan_speed" case 13: return "", "power" } return "", "sensor" } func normalizeCIMHealthStatus(v string) string { switch parseIntLoose(v) { case 1, 2, 5, 15: return "ok" case 3, 4, 10: return "warning" case 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 18: return "critical" default: return normalizeStatus(v) } } func firstNonEmpty(values ...string) string { for _, v := range values { v = strings.TrimSpace(v) if v != "" { return v } } return "" } func setIfEmpty(dst *string, value string) { if strings.TrimSpace(*dst) != "" { return } *dst = strings.TrimSpace(value) } func nonEmptyStrings(values []string) []string { out := make([]string, 0, len(values)) for _, v := range values { v = strings.TrimSpace(v) if v != "" { out = append(out, v) } } return out } func dedupeStorage(items []models.Storage) []models.Storage { out := make([]models.Storage, 0, len(items)) indexByKey := make(map[string]int) for _, item := range items { key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.Slot, item.Model))) if key == "" { continue } if idx, ok := indexByKey[key]; ok { mergeStorage(&out[idx], item) continue } indexByKey[key] = len(out) out = append(out, item) } return out } func mergeStorage(dst *models.Storage, src models.Storage) { setIfEmpty(&dst.Slot, src.Slot) setIfEmpty(&dst.Type, src.Type) setIfEmpty(&dst.Model, src.Model) if dst.SizeGB == 0 { dst.SizeGB = src.SizeGB } setIfEmpty(&dst.SerialNumber, src.SerialNumber) setIfEmpty(&dst.Manufacturer, src.Manufacturer) setIfEmpty(&dst.Firmware, src.Firmware) setIfEmpty(&dst.Interface, src.Interface) if src.Present { dst.Present = true } setIfEmpty(&dst.Location, src.Location) setIfEmpty(&dst.Status, src.Status) dst.Details = mergeDellDetails(dst.Details, src.Details) } func dedupeVolumes(items []models.StorageVolume) []models.StorageVolume { out := make([]models.StorageVolume, 0, len(items)) seen := make(map[string]int) for _, item := range items { key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.ID, item.Name))) if key == "" { continue } if idx, ok := seen[key]; ok { mergeVolume(&out[idx], item) continue } seen[key] = len(out) out = append(out, item) } return out } func mergeVolume(dst *models.StorageVolume, src models.StorageVolume) { setIfEmpty(&dst.ID, src.ID) setIfEmpty(&dst.Name, src.Name) setIfEmpty(&dst.Controller, src.Controller) setIfEmpty(&dst.RAIDLevel, src.RAIDLevel) if dst.SizeGB == 0 { dst.SizeGB = src.SizeGB } if dst.CapacityBytes == 0 { dst.CapacityBytes = src.CapacityBytes } setIfEmpty(&dst.Status, src.Status) } func dedupePSU(items []models.PSU) []models.PSU { out := make([]models.PSU, 0, len(items)) seen := make(map[string]int) for _, item := range items { key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.Slot, item.Model))) if key == "" { continue } if idx, ok := seen[key]; ok { mergePSU(&out[idx], item) continue } seen[key] = len(out) out = append(out, item) } return out } func mergePSU(dst *models.PSU, src models.PSU) { setIfEmpty(&dst.Slot, src.Slot) if src.Present { dst.Present = true } setIfEmpty(&dst.Model, src.Model) setIfEmpty(&dst.Description, src.Description) setIfEmpty(&dst.Vendor, src.Vendor) if dst.WattageW == 0 { dst.WattageW = src.WattageW } setIfEmpty(&dst.SerialNumber, src.SerialNumber) setIfEmpty(&dst.PartNumber, src.PartNumber) setIfEmpty(&dst.Firmware, src.Firmware) setIfEmpty(&dst.Status, src.Status) if dst.InputVoltage == 0 { dst.InputVoltage = src.InputVoltage } setIfEmpty(&dst.InputType, src.InputType) dst.Details = mergeDellDetails(dst.Details, src.Details) } func mergeDellDetails(primary, secondary map[string]any) map[string]any { if len(secondary) == 0 { return primary } if primary == nil { primary = make(map[string]any, len(secondary)) } for key, value := range secondary { if _, ok := primary[key]; !ok { primary[key] = value } } return primary } func dedupeNetworkAdapters(items []models.NetworkAdapter) []models.NetworkAdapter { out := make([]models.NetworkAdapter, 0, len(items)) seen := make(map[string]int) for _, item := range items { key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, strings.Join(item.MACAddresses, ","), item.Slot))) if key == "" { continue } if idx, ok := seen[key]; ok { mergeNetworkAdapter(&out[idx], item) continue } seen[key] = len(out) out = append(out, item) } return out } func mergeNetworkAdapter(dst *models.NetworkAdapter, src models.NetworkAdapter) { setIfEmpty(&dst.Slot, src.Slot) setIfEmpty(&dst.Location, src.Location) if src.Present { dst.Present = true } setIfEmpty(&dst.Model, src.Model) setIfEmpty(&dst.Description, src.Description) setIfEmpty(&dst.Vendor, src.Vendor) if dst.VendorID == 0 { dst.VendorID = src.VendorID } if dst.DeviceID == 0 { dst.DeviceID = src.DeviceID } setIfEmpty(&dst.SerialNumber, src.SerialNumber) setIfEmpty(&dst.PartNumber, src.PartNumber) setIfEmpty(&dst.Firmware, src.Firmware) if dst.PortCount == 0 { dst.PortCount = src.PortCount } setIfEmpty(&dst.Status, src.Status) for _, mac := range src.MACAddresses { mac = strings.TrimSpace(mac) if mac == "" { continue } if !containsStringFold(dst.MACAddresses, mac) { dst.MACAddresses = append(dst.MACAddresses, mac) } } } func nicCardsFromAdapters(items []models.NetworkAdapter) []models.NIC { out := make([]models.NIC, 0, len(items)) for _, item := range items { out = append(out, models.NIC{ Name: firstNonEmpty(item.Slot, item.Location, "NIC"), Model: item.Model, Description: item.Description, MACAddress: firstNonEmpty(item.MACAddresses...), SerialNumber: item.SerialNumber, }) } return out } // removePCIeOverlappingWithGPUs drops PCIe entries that duplicate a GPU already // captured from DCIM_VideoView. Dell TSR lists GPUs in both DCIM_VideoView and // DCIM_PCIDeviceView; the VideoView record is authoritative (has serial, firmware, // temperature) so the PCIe duplicate must be removed. func removePCIeOverlappingWithGPUs(pcie []models.PCIeDevice, gpus []models.GPU) []models.PCIeDevice { if len(gpus) == 0 { return pcie } gpuSlots := make(map[string]struct{}, len(gpus)) gpuBDFs := make(map[string]struct{}, len(gpus)) for _, g := range gpus { if s := strings.ToLower(strings.TrimSpace(g.Slot)); s != "" { gpuSlots[s] = struct{}{} } if b := strings.ToLower(strings.TrimSpace(g.BDF)); b != "" { gpuBDFs[b] = struct{}{} } } out := make([]models.PCIeDevice, 0, len(pcie)) for _, p := range pcie { slot := strings.ToLower(strings.TrimSpace(p.Slot)) bdf := strings.ToLower(strings.TrimSpace(p.BDF)) if _, ok := gpuSlots[slot]; ok && slot != "" { continue } if _, ok := gpuBDFs[bdf]; ok && bdf != "" { continue } out = append(out, p) } return out } func dedupePCIe(items []models.PCIeDevice) []models.PCIeDevice { out := make([]models.PCIeDevice, 0, len(items)) seen := make(map[string]int) for _, item := range items { key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.BDF, item.Slot, item.Description))) if key == "" { continue } if idx, ok := seen[key]; ok { mergePCIe(&out[idx], item) continue } seen[key] = len(out) out = append(out, item) } return out } func mergePCIe(dst *models.PCIeDevice, src models.PCIeDevice) { setIfEmpty(&dst.Slot, src.Slot) setIfEmpty(&dst.Description, src.Description) if dst.VendorID == 0 { dst.VendorID = src.VendorID } if dst.DeviceID == 0 { dst.DeviceID = src.DeviceID } setIfEmpty(&dst.BDF, src.BDF) setIfEmpty(&dst.DeviceClass, src.DeviceClass) setIfEmpty(&dst.Manufacturer, src.Manufacturer) setIfEmpty(&dst.Status, src.Status) } func dedupeCPU(items []models.CPU) []models.CPU { out := make([]models.CPU, 0, len(items)) seen := make(map[string]int) for _, item := range items { key := strings.ToLower(strings.TrimSpace(firstNonEmpty(fmt.Sprintf("%d", item.Socket), item.PPIN, item.Model))) if key == "" { continue } if idx, ok := seen[key]; ok { mergeCPU(&out[idx], item) continue } seen[key] = len(out) out = append(out, item) } return out } func dedupeGPU(items []models.GPU) []models.GPU { out := make([]models.GPU, 0, len(items)) seen := make(map[string]int) for _, item := range items { key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.UUID, item.BDF, item.Slot, item.Model))) if key == "" { continue } if idx, ok := seen[key]; ok { mergeGPU(&out[idx], item) continue } seen[key] = len(out) out = append(out, item) } return out } func mergeGPU(dst *models.GPU, src models.GPU) { setIfEmpty(&dst.Slot, src.Slot) setIfEmpty(&dst.Location, src.Location) setIfEmpty(&dst.Model, src.Model) setIfEmpty(&dst.Description, src.Description) setIfEmpty(&dst.Manufacturer, src.Manufacturer) if dst.VendorID == 0 { dst.VendorID = src.VendorID } if dst.DeviceID == 0 { dst.DeviceID = src.DeviceID } setIfEmpty(&dst.BDF, src.BDF) setIfEmpty(&dst.UUID, src.UUID) setIfEmpty(&dst.SerialNumber, src.SerialNumber) setIfEmpty(&dst.PartNumber, src.PartNumber) setIfEmpty(&dst.Firmware, src.Firmware) setIfEmpty(&dst.Status, src.Status) } func mergeCPU(dst *models.CPU, src models.CPU) { if dst.Socket == 0 { dst.Socket = src.Socket } setIfEmpty(&dst.Model, src.Model) setIfEmpty(&dst.Description, src.Description) if dst.Cores == 0 { dst.Cores = src.Cores } if dst.Threads == 0 { dst.Threads = src.Threads } if dst.FrequencyMHz == 0 { dst.FrequencyMHz = src.FrequencyMHz } if dst.MaxFreqMHz == 0 { dst.MaxFreqMHz = src.MaxFreqMHz } setIfEmpty(&dst.PPIN, src.PPIN) setIfEmpty(&dst.Status, src.Status) } func dedupeDIMM(items []models.MemoryDIMM) []models.MemoryDIMM { out := make([]models.MemoryDIMM, 0, len(items)) seen := make(map[string]int) for _, item := range items { key := strings.ToLower(strings.TrimSpace(firstNonEmpty(item.SerialNumber, item.Slot))) if key == "" { continue } if idx, ok := seen[key]; ok { mergeDIMM(&out[idx], item) continue } seen[key] = len(out) out = append(out, item) } return out } func mergeDIMM(dst *models.MemoryDIMM, src models.MemoryDIMM) { setIfEmpty(&dst.Slot, src.Slot) setIfEmpty(&dst.Location, src.Location) if src.Present { dst.Present = true } if dst.SizeMB == 0 { dst.SizeMB = src.SizeMB } setIfEmpty(&dst.Type, src.Type) setIfEmpty(&dst.Technology, src.Technology) if dst.MaxSpeedMHz == 0 { dst.MaxSpeedMHz = src.MaxSpeedMHz } if dst.CurrentSpeedMHz == 0 { dst.CurrentSpeedMHz = src.CurrentSpeedMHz } setIfEmpty(&dst.Manufacturer, src.Manufacturer) setIfEmpty(&dst.SerialNumber, src.SerialNumber) setIfEmpty(&dst.PartNumber, src.PartNumber) setIfEmpty(&dst.Status, src.Status) } func dedupeFirmware(items []models.FirmwareInfo) []models.FirmwareInfo { out := make([]models.FirmwareInfo, 0, len(items)) seen := make(map[string]struct{}) for _, item := range items { key := strings.ToLower(strings.TrimSpace(item.DeviceName + "|" + item.Version + "|" + item.Description)) if key == "" || strings.TrimSpace(item.Version) == "" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, item) } sort.SliceStable(out, func(i, j int) bool { return strings.ToLower(out[i].DeviceName) < strings.ToLower(out[j].DeviceName) }) return out } func dedupeSensors(items []models.SensorReading) []models.SensorReading { out := make([]models.SensorReading, 0, len(items)) seen := make(map[string]int) for _, item := range items { name := strings.TrimSpace(item.Name) if name == "" { continue } key := strings.ToLower(strings.TrimSpace(item.Type + "|" + name)) if idx, ok := seen[key]; ok { mergeSensor(&out[idx], item) continue } seen[key] = len(out) out = append(out, item) } return out } func mergeSensor(dst *models.SensorReading, src models.SensorReading) { setIfEmpty(&dst.Name, src.Name) setIfEmpty(&dst.Type, src.Type) if dst.Value == 0 && src.Value != 0 { dst.Value = src.Value } setIfEmpty(&dst.Unit, src.Unit) setIfEmpty(&dst.RawValue, src.RawValue) setIfEmpty(&dst.Status, src.Status) } func containsStringFold(items []string, value string) bool { for _, item := range items { if strings.EqualFold(strings.TrimSpace(item), strings.TrimSpace(value)) { return true } } return false }