init
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
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..<min(48, registers.count) {
|
||||
let millivolts = Int(registers[i])
|
||||
if millivolts > 500 && millivolts < 5000 {
|
||||
cellMillivolts[i + 1] = millivolts
|
||||
}
|
||||
}
|
||||
|
||||
// Register 48–55: Temperaturfühler mit Offset 40.
|
||||
sensorTemperatures.removeAll(keepingCapacity: true)
|
||||
for i in 48..<min(56, registers.count) {
|
||||
let raw = Int(registers[i])
|
||||
if raw > 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user