Solar-Integration und dev_watch-Geräte zusammenführen

solar-integration (aus dem separaten VanAligneiOS-Repo) und dev_watch
haben unabhängige Git-Historien, decken aber überlappende und sich
ergänzende Funktionen ab. Übernommen aus solar-integration: Votronic-
Solar-ESP-Anbindung samt Geräterolle, die Live-Activity/Widget-Extension
fürs Sperrbildschirm/Dynamic-Island/CarPlay, das Querformat-Layout für
Libelle/Fahrzeug-Ansicht und Ausrichtungs-Assistent, sowie die
mehreren Fahrzeuggrafik-Stile (Vanster/California). Beibehalten aus
dev_watch: alle zusätzlichen Geräteprotokolle (Daly-/JBD-BMS, Alpicool-
Kühlbox, WattCycle, Victron), die dort zwischenzeitlich entstanden.

Die Xcode-Projektdatei wurde von Hand um die neue Widget-Extension samt
SharedActivity-Gruppe erweitert (Datei-synchronisierte Gruppen, kein
App-Group-Entitlement nötig). Build für App, Watch und Widget-Extension
geprüft (Debug und Release).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
fototeddy
2026-09-06 21:00:14 +02:00
co-authored by Claude Sonnet 5
parent 812874baef
commit e9b9c5bcd5
42 changed files with 2592 additions and 214 deletions
+8 -1
View File
@@ -9,6 +9,7 @@ enum DeviceRole: String, Codable, CaseIterable, Identifiable, Sendable {
case bms
case fridge
case leveling
case solar
var id: String { rawValue }
@@ -20,6 +21,7 @@ enum DeviceRole: String, Codable, CaseIterable, Identifiable, Sendable {
case .bms: return "Batterie / BMS"
case .fridge: return "Kühlbox"
case .leveling: return "Nivellierung"
case .solar: return "VotronicSolarESP"
}
}
@@ -31,6 +33,7 @@ enum DeviceRole: String, Codable, CaseIterable, Identifiable, Sendable {
case .bms: return "battery.100percent.bolt"
case .fridge: return "refrigerator"
case .leveling: return "level"
case .solar: return "sun.max"
}
}
@@ -50,7 +53,7 @@ enum DeviceRole: String, Codable, CaseIterable, Identifiable, Sendable {
/// braucht eine echte GATT-Verbindung.
var transport: DeviceTransport {
switch self {
case .bms, .fridge, .leveling: return .connect
case .bms, .fridge, .leveling, .solar: return .connect
default: return .advertisement
}
}
@@ -98,6 +101,8 @@ struct ConfiguredDevice: Identifiable, Codable, Hashable, Sendable {
var fridgeZoneMode: FridgeZoneMode = .automatic
/// Nur für den Neigungsmesser: wie er im Fahrzeug sitzt.
var sensorOrientation = SensorOrientation.identity
/// Nur für den Neigungsmesser: welche Fahrzeuggrafik die Fahrzeug-Ansicht zeigt.
var vehicleGraphicStyle = VehicleGraphicStyle.vanster
init(id: UUID = UUID(),
name: String,
@@ -128,5 +133,7 @@ struct ConfiguredDevice: Identifiable, Codable, Hashable, Sendable {
?? .automatic
sensorOrientation = try container.decodeIfPresent(SensorOrientation.self,
forKey: .sensorOrientation) ?? .identity
vehicleGraphicStyle = try container.decodeIfPresent(VehicleGraphicStyle.self,
forKey: .vehicleGraphicStyle) ?? .vanster
}
}
+52
View File
@@ -0,0 +1,52 @@
import Foundation
/// Welche Silhouette die Fahrzeug-Ansicht des Neigungsmessers zeigt.
///
/// Jeder Fall ist ein zusammengehöriges Bilderset aus Seiten- und
/// Heckansicht in Assets.xcassets, beide als Vorlage ("template")
/// exportiert, damit `foregroundStyle` sie einfärben kann. Im Picker wird
/// nur die Seitenansicht gezeigt.
enum VehicleGraphicStyle: String, CaseIterable, Identifiable, Codable, Sendable {
case vanster
case california
var id: String { rawValue }
var title: String {
switch self {
case .vanster: return "Vanster"
case .california: return "California"
}
}
var sideImageName: String {
switch self {
case .vanster: return "VansterSide"
case .california: return "CaliforniaSide"
}
}
/// Für California liegt noch keine eigene Heckansicht vor, deshalb
/// vorerst die von Vanster.
var rearImageName: String {
switch self {
case .vanster, .california: return "VansterRear"
}
}
/// Seitenverhältnis der jeweiligen Bilder ohne das würde eine Grafik
/// mit anderen Proportionen in der gemeinsamen Bildhülle verzerrt oder
/// ungewollt verkleinert erscheinen.
var sideAspect: Double {
switch self {
case .vanster: return VehicleTilt.sideAspect
case .california: return 1
}
}
var rearAspect: Double {
switch self {
case .vanster, .california: return VehicleTilt.rearAspect
}
}
}
+48
View File
@@ -0,0 +1,48 @@
import Foundation
/// Zustand des Solarladereglers.
struct VotronicSolarESPState: Equatable, Codable, Sendable {
var batteryVoltage: Double?
var pvVoltage: Double?
var pvCurrent: Double?
var pvPower: Double?
var controllerTemperature: Double?
var isBatteryCharging: Bool?
var isBatteryDischarging: Bool?
var isControllerActive: Bool?
var isCurrentLimited: Bool?
var hasReading: Bool {
batteryVoltage != nil || pvVoltage != nil || pvCurrent != nil || pvPower != nil
}
/// Kurzer Klartext, wie bei den übrigen Geräten als "Zustand" angezeigt.
var stateText: String? {
guard isControllerActive != nil else { return nil }
if isBatteryCharging == true { return "Lädt" }
if isControllerActive == true { return "Aktiv" }
return "Inaktiv"
}
func snapshot(deviceID: UUID, rssi: Int?) -> DeviceSnapshot {
var snapshot = DeviceSnapshot(deviceID: deviceID, timestamp: Date(), rssi: rssi)
snapshot.metrics = [
Metric("pv_power", "Solarleistung", pvPower, unit: "W", precision: 0, primary: true),
Metric("pv_voltage", "PV-Spannung", pvVoltage, unit: "V", precision: 1),
Metric("pv_current", "PV-Strom", pvCurrent, unit: "A", precision: 1),
Metric("battery_voltage", "Batteriespannung", batteryVoltage, unit: "V", precision: 2),
]
if let controllerTemperature {
snapshot.metrics.append(
Metric("controller_temperature", "Reglertemperatur", controllerTemperature,
unit: "°C", precision: 0)
)
}
snapshot.state = stateText
if isCurrentLimited == true {
snapshot.offReasons = ["PV-Strombegrenzung aktiv"]
}
return snapshot
}
}