import Foundation /// Sammelt die Antworten eines Daly-BMS. Das klassische Protokoll verteilt die /// Werte auf mehrere Rahmen, deshalb wird hier über Abfragerunden hinweg /// akkumuliert und erst am Ende ein Snapshot gebaut. struct DalyState { var totalVoltage: Double? var current: Double? var soc: Double? var maxCellMillivolts: Int? var maxCellNumber: Int? var minCellMillivolts: Int? var minCellNumber: Int? var maxTemperature: Double? var minTemperature: Double? var chargeMOSOn: Bool? var dischargeMOSOn: Bool? var chargeDischargeStatus: UInt8? var remainingCapacityAh: Double? var cellCount: Int? var temperatureSensorCount: Int? var cycles: Int? /// Zellnummer (1-basiert) → Spannung in Millivolt. var cellMillivolts: [Int: Int] = [:] /// Sensornummer (1-basiert) → Temperatur in °C. var sensorTemperatures: [Int: Double] = [:] /// Letzte Rohantwort, für die Diagnoseansicht. var lastRawResponse: Data? var usesModbus = false // MARK: - Klassisches Protokoll mutating func apply(_ frame: DalyProtocol.Frame) { let d = frame.payload func u16(_ i: Int) -> Int { Int(d[i]) << 8 | Int(d[i + 1]) } switch frame.command { case 0x90: totalVoltage = Double(u16(0)) * 0.1 // Strom mit Offset 30000, damit Entladung negativ dargestellt wird. current = Double(u16(4) - 30000) * 0.1 soc = Double(u16(6)) * 0.1 case 0x91: maxCellMillivolts = u16(0) maxCellNumber = Int(d[2]) minCellMillivolts = u16(3) minCellNumber = Int(d[5]) case 0x92: maxTemperature = Double(Int(d[0]) - 40) minTemperature = Double(Int(d[2]) - 40) case 0x93: chargeDischargeStatus = d[0] chargeMOSOn = d[1] == 1 dischargeMOSOn = d[2] == 1 let capacityMilliAh = (UInt32(d[4]) << 24) | (UInt32(d[5]) << 16) | (UInt32(d[6]) << 8) | UInt32(d[7]) remainingCapacityAh = Double(capacityMilliAh) / 1000 case 0x94: cellCount = Int(d[0]) temperatureSensorCount = Int(d[1]) cycles = u16(6) case 0x95: // d[0] = Rahmennummer (1-basiert), danach drei Zellen à 2 Byte. let frameNumber = Int(d[0]) guard frameNumber > 0 else { break } for slot in 0..<3 { let cell = (frameNumber - 1) * 3 + slot + 1 let millivolts = u16(1 + slot * 2) if millivolts > 0 && millivolts < 6000 { cellMillivolts[cell] = millivolts } } case 0x96: let frameNumber = Int(d[0]) guard frameNumber > 0 else { break } for slot in 0..<7 { let sensor = (frameNumber - 1) * 7 + slot + 1 let raw = Int(d[1 + slot]) if raw != 0 { sensorTemperatures[sensor] = Double(raw - 40) } } default: break } } // MARK: - Modbus-Protokoll /// Registerbelegung der neueren Daly-BMS. /// /// Achtung: Dieses Mapping variiert zwischen Firmwareständen. Die /// Detailansicht zeigt deshalb die Rohantwort an, damit sich die Belegung /// am realen Gerät nachprüfen lässt. mutating func apply(registers: [UInt16]) { usesModbus = true func reg(_ i: Int) -> UInt16? { i < registers.count ? registers[i] : nil } // Register 0–47: Zellspannungen in mV, unbenutzte Plätze sind 0. cellMillivolts.removeAll(keepingCapacity: true) for i in 0.. 500 && millivolts < 5000 { cellMillivolts[i + 1] = millivolts } } // Register 48–55: Temperaturfühler mit Offset 40. sensorTemperatures.removeAll(keepingCapacity: true) for i in 48.. 0 && raw < 200 { sensorTemperatures[i - 47] = Double(raw - 40) } } if let v = reg(56), v > 0 { totalVoltage = Double(v) * 0.1 } if let c = reg(57) { current = (Double(c) - 30000) * 0.1 } if let s = reg(58), s <= 1000 { soc = Double(s) * 0.1 } maxCellMillivolts = cellMillivolts.values.max() minCellMillivolts = cellMillivolts.values.min() maxCellNumber = cellMillivolts.max(by: { $0.value < $1.value })?.key minCellNumber = cellMillivolts.min(by: { $0.value < $1.value })?.key maxTemperature = sensorTemperatures.values.max() minTemperature = sensorTemperatures.values.min() cellCount = cellMillivolts.isEmpty ? nil : cellMillivolts.count temperatureSensorCount = sensorTemperatures.isEmpty ? nil : sensorTemperatures.count } // MARK: - Ausgabe var hasUsableData: Bool { totalVoltage != nil || soc != nil || !cellMillivolts.isEmpty } func snapshot(deviceID: UUID, rssi: Int?) -> DeviceSnapshot { var snapshot = DeviceSnapshot(deviceID: deviceID, timestamp: Date(), rssi: rssi) var metrics: [Metric] = [ Metric("soc", "Ladezustand", soc, unit: "%", precision: 1, primary: true), Metric("voltage", "Spannung", totalVoltage, unit: "V", precision: 2), Metric("current", "Strom", current, unit: "A", precision: 1), ] if let v = totalVoltage, let a = current { metrics.append(Metric("power", "Leistung", v * a, unit: "W", precision: 0)) } if let capacity = remainingCapacityAh { metrics.append(Metric("capacity", "Restkapazität", capacity, unit: "Ah", precision: 1)) } if let maxV = maxCellMillivolts, let minV = minCellMillivolts { metrics.append(Metric("cell_delta", "Zell-Differenz", Double(maxV - minV), unit: "mV", precision: 0)) metrics.append(Metric("cell_max", "Höchste Zelle" + numberSuffix(maxCellNumber), Double(maxV) / 1000, unit: "V", precision: 3)) metrics.append(Metric("cell_min", "Niedrigste Zelle" + numberSuffix(minCellNumber), Double(minV) / 1000, unit: "V", precision: 3)) } if let maxTemperature { metrics.append(Metric("temp_max", "Temperatur", maxTemperature, unit: "°C", precision: 0)) } if let minTemperature, minTemperature != maxTemperature { metrics.append(Metric("temp_min", "Temperatur min.", minTemperature, unit: "°C", precision: 0)) } if let cycles { metrics.append(Metric("cycles", "Ladezyklen", Double(cycles), unit: "", precision: 0)) } snapshot.metrics = metrics snapshot.state = stateText snapshot.cellVoltages = cellMillivolts .sorted { $0.key < $1.key } .map { Double($0.value) / 1000 } snapshot.temperatures = sensorTemperatures.sorted { $0.key < $1.key }.map(\.value) var warnings: [String] = [] if chargeMOSOn == false { warnings.append("Lade-MOSFET aus") } if dischargeMOSOn == false { warnings.append("Entlade-MOSFET aus") } snapshot.offReasons = warnings return snapshot } private func numberSuffix(_ number: Int?) -> String { number.map { " (Zelle \($0))" } ?? "" } private var stateText: String? { if let status = chargeDischargeStatus { switch status { case 0: return "Ruhend" case 1: return "Lädt" case 2: return "Entlädt" default: break } } guard let current else { return nil } if current > 0.3 { return "Lädt" } if current < -0.3 { return "Entlädt" } return "Ruhend" } }