Compare commits

...

2 Commits

Author SHA1 Message Date
Stefan Lange-Hegermann
0989c68aa7 well designed system overview 2025-10-23 17:23:38 +02:00
Stefan Lange-Hegermann
51d85cc352 chargers in overview 2025-10-23 15:27:22 +02:00
8 changed files with 262 additions and 21 deletions

View File

@@ -405,7 +405,7 @@
CODE_SIGN_ENTITLEMENTS = Cable/Cable.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 28;
CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = RE4FXQ754N;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;
@@ -440,7 +440,7 @@
CODE_SIGN_ENTITLEMENTS = Cable/Cable.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 28;
CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = RE4FXQ754N;
ENABLE_APP_SANDBOX = YES;
ENABLE_PREVIEWS = YES;

View File

@@ -108,6 +108,10 @@
"battery.onboarding.title" = "Add your first battery";
"battery.onboarding.subtitle" = "Track your bank's capacity and chemistry to keep runtime expectations in check.";
"battery.bank.badge.voltage" = "Voltage";
"overview.chargers.header.title" = "Charger Overview";
"overview.chargers.empty.title" = "No chargers configured yet";
"overview.chargers.empty.subtitle" = "Add shore power, DC-DC, or solar chargers to understand your charging capacity.";
"overview.chargers.empty.create" = "Add Charger";
"battery.bank.badge.capacity" = "Capacity";
"battery.bank.badge.energy" = "Energy";
"battery.bank.banner.voltage" = "Voltage mismatch detected";

View File

@@ -282,11 +282,14 @@ struct LoadsView: View {
system: system,
loads: savedLoads,
batteries: savedBatteries,
chargers: savedChargers,
onSelectLoads: { selectedComponentTab = .components },
onSelectBatteries: { selectedComponentTab = .batteries },
onSelectChargers: { selectedComponentTab = .chargers },
onCreateLoad: { createNewLoad() },
onBrowseLibrary: { showingComponentLibrary = true },
onCreateBattery: { startBatteryConfiguration() }
onCreateBattery: { startBatteryConfiguration() },
onCreateCharger: { startChargerConfiguration() }
)
.accessibilityIdentifier("system-overview")
}

View File

@@ -7,11 +7,14 @@ struct SystemOverviewView: View {
let system: ElectricalSystem
let loads: [SavedLoad]
let batteries: [SavedBattery]
let chargers: [SavedCharger]
let onSelectLoads: () -> Void
let onSelectBatteries: () -> Void
let onSelectChargers: () -> Void
let onCreateLoad: () -> Void
let onBrowseLibrary: () -> Void
let onCreateBattery: () -> Void
let onCreateCharger: () -> Void
var body: some View {
ScrollView {
@@ -19,6 +22,7 @@ struct SystemOverviewView: View {
systemCard
loadsCard
batteriesCard
chargersCard
}
.padding(.horizontal, 16)
.padding(.vertical, 20)
@@ -63,13 +67,19 @@ struct SystemOverviewView: View {
label: loadsCountLabel,
value: "\(loads.count)",
tint: .blue
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .green
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.fill",
label: chargerCountLabel,
value: "\(chargers.count)",
tint: .orange
).frame(maxWidth: .infinity, alignment: .leading)
}
VStack(alignment: .leading, spacing: 12) {
@@ -78,13 +88,19 @@ struct SystemOverviewView: View {
label: loadsCountLabel,
value: "\(loads.count)",
tint: .blue
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .green
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.fill",
label: chargerCountLabel,
value: "\(chargers.count)",
tint: .orange
).frame(maxWidth: .infinity, alignment: .leading)
}
}
@@ -163,19 +179,19 @@ struct SystemOverviewView: View {
label: loadsCountLabel,
value: "\(loads.count)",
tint: .blue
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.fill",
label: loadsCurrentLabel,
value: formattedCurrent(totalCurrent),
tint: .orange
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "gauge.medium",
label: loadsPowerLabel,
value: formattedPower(totalPower),
tint: .green
)
).frame(maxWidth: .infinity, alignment: .leading)
}
VStack(alignment: .leading, spacing: 12) {
@@ -184,19 +200,19 @@ struct SystemOverviewView: View {
label: loadsCountLabel,
value: "\(loads.count)",
tint: .blue
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.fill",
label: loadsCurrentLabel,
value: formattedCurrent(totalCurrent),
tint: .orange
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "gauge.medium",
label: loadsPowerLabel,
value: formattedPower(totalPower),
tint: .green
)
).frame(maxWidth: .infinity, alignment: .leading)
}
}
@@ -290,19 +306,19 @@ struct SystemOverviewView: View {
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.circle",
label: batteryEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
).frame(maxWidth: .infinity, alignment: .leading)
}
VStack(alignment: .leading, spacing: 12) {
@@ -311,19 +327,19 @@ struct SystemOverviewView: View {
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.circle",
label: batteryEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
).frame(maxWidth: .infinity, alignment: .leading)
}
}
}
@@ -339,6 +355,83 @@ struct SystemOverviewView: View {
.buttonStyle(.plain)
}
private var chargersCard: some View {
Button(action: onSelectChargers) {
VStack(alignment: .leading, spacing: 16) {
Text(chargerSummaryTitle)
.font(.headline.weight(.semibold))
if chargers.isEmpty {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(chargerEmptyTitle)
.font(.subheadline.weight(.semibold))
Text(chargerEmptySubtitle)
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Button(action: onCreateCharger) {
Label(chargerEmptyCreateAction, systemImage: "plus")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
} else {
ViewThatFits(in: .horizontal) {
HStack(spacing: 16) {
chargerMetricsContent
}
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: 2),
alignment: .leading,
spacing: 16
) {
chargerMetricsContent
}
VStack(alignment: .leading, spacing: 12) {
chargerMetricsContent
}
}
}
}
.padding(.horizontal, 16)
.padding(.vertical, 18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Color(.systemBackground))
)
}
.buttonStyle(.plain)
}
@ViewBuilder
private var chargerMetricsContent: some View {
summaryMetric(
icon: "bolt.fill",
label: chargerCountLabel,
value: "\(chargers.count)",
tint: .blue
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "gauge.medium",
label: chargerCurrentLabel,
value: formattedCurrent(totalChargerCurrent),
tint: .orange
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.circle",
label: chargerPowerLabel,
value: formattedPower(totalChargerPower),
tint: .green
).frame(maxWidth: .infinity, alignment: .leading)
}
private var loadStatus: LoadConfigurationStatus? {
guard !loads.isEmpty else { return nil }
let incomplete = loads.filter { load in
@@ -374,6 +467,25 @@ struct SystemOverviewView: View {
}
}
private var totalChargerCurrent: Double {
chargers.reduce(0) { result, charger in
result + max(0, charger.maxCurrentAmps)
}
}
private var totalChargerPower: Double {
chargers.reduce(0) { result, charger in
result + max(0, charger.effectivePowerWatts)
}
}
private var representativeChargerOutput: Double? {
let outputs = chargers.map { $0.outputVoltage }.filter { $0 > 0 }
guard !outputs.isEmpty else { return nil }
let total = outputs.reduce(0, +)
return total / Double(outputs.count)
}
private var batteryWarning: BatteryWarning? {
guard batteries.count > 1 else { return nil }
@@ -488,6 +600,11 @@ struct SystemOverviewView: View {
return "\(numberString) W"
}
private func formattedChargerOutput(_ value: Double?) -> String {
guard let value, value > 0 else { return "" }
return formattedValue(value, unit: "V")
}
private func formattedValue(_ value: Double, unit: String) -> String {
let numberString = Self.numberFormatter.string(from: NSNumber(value: value)) ?? String(format: "%.1f", value)
return "\(numberString) \(unit)"
@@ -659,6 +776,78 @@ struct SystemOverviewView: View {
)
}
private var chargerSummaryTitle: String {
NSLocalizedString(
"overview.chargers.header.title",
bundle: .main,
value: "Charger Overview",
comment: "Title for the chargers summary section"
)
}
private var chargerCountLabel: String {
NSLocalizedString(
"chargers.summary.metric.count",
bundle: .main,
value: "Chargers",
comment: "Label for number of chargers metric"
)
}
private var chargerOutputLabel: String {
NSLocalizedString(
"chargers.summary.metric.output",
bundle: .main,
value: "Output Voltage",
comment: "Label for representative output voltage metric"
)
}
private var chargerCurrentLabel: String {
NSLocalizedString(
"chargers.summary.metric.current",
bundle: .main,
value: "Charge Rate",
comment: "Label for total charger current metric"
)
}
private var chargerPowerLabel: String {
NSLocalizedString(
"chargers.summary.metric.power",
bundle: .main,
value: "Charge Power",
comment: "Label for total charger power metric"
)
}
private var chargerEmptyTitle: String {
NSLocalizedString(
"overview.chargers.empty.title",
bundle: .main,
value: "No chargers configured yet",
comment: "Title shown when no chargers are configured"
)
}
private var chargerEmptySubtitle: String {
NSLocalizedString(
"overview.chargers.empty.subtitle",
bundle: .main,
value: "Add shore power, DC-DC, or solar chargers to understand your charging capacity.",
comment: "Subtitle shown when no chargers are configured"
)
}
private var chargerEmptyCreateAction: String {
NSLocalizedString(
"overview.chargers.empty.create",
bundle: .main,
value: "Add Charger",
comment: "Button title to create a charger from the overview"
)
}
private var systemOverviewTitle: String {
NSLocalizedString(
"overview.system.header.title",
@@ -802,15 +991,41 @@ struct SystemOverviewView: View {
)
]
let chargers: [SavedCharger] = [
SavedCharger(
name: "Shore Power",
inputVoltage: 230,
outputVoltage: 14.4,
maxCurrentAmps: 40,
maxPowerWatts: 580,
iconName: "powerplug",
colorName: "orange",
system: system
),
SavedCharger(
name: "DC-DC Charger",
inputVoltage: 12.8,
outputVoltage: 14.2,
maxCurrentAmps: 30,
maxPowerWatts: 0,
iconName: "bolt.circle",
colorName: "blue",
system: system
)
]
SystemOverviewView(
system: system,
loads: loads,
batteries: batteries,
chargers: chargers,
onSelectLoads: {},
onSelectBatteries: {},
onSelectChargers: {},
onCreateLoad: {},
onBrowseLibrary: {},
onCreateBattery: {}
onCreateBattery: {},
onCreateCharger: {}
)
.padding()
}
@@ -827,11 +1042,14 @@ struct SystemOverviewView: View {
system: system,
loads: [],
batteries: [],
chargers: [],
onSelectLoads: {},
onSelectBatteries: {},
onSelectChargers: {},
onCreateLoad: {},
onBrowseLibrary: {},
onCreateBattery: {}
onCreateBattery: {},
onCreateCharger: {}
)
.padding()
}

View File

@@ -176,6 +176,10 @@
"battery.onboarding.title" = "Füge deine erste Batterie hinzu";
"battery.onboarding.subtitle" = "Behalte Kapazität und Chemie deiner Batterien im Blick, um die Laufzeit im Griff zu behalten.";
"battery.bank.badge.voltage" = "Spannung";
"overview.chargers.header.title" = "Ladegeräte";
"overview.chargers.empty.title" = "Noch keine Ladegeräte konfiguriert";
"overview.chargers.empty.subtitle" = "Füge Landstrom-, DC-DC- oder Solarladegeräte hinzu, um deine Ladeleistung zu verstehen.";
"overview.chargers.empty.create" = "Ladegerät hinzufügen";
"battery.bank.badge.capacity" = "Kapazität";
"battery.bank.badge.energy" = "Energie";
"battery.bank.banner.voltage" = "Spannungsabweichung erkannt";

View File

@@ -175,6 +175,10 @@
"battery.onboarding.title" = "Añade tu primera batería";
"battery.onboarding.subtitle" = "Controla la capacidad y la química del banco para mantener tus tiempos de autonomía bajo control.";
"battery.bank.badge.voltage" = "Voltaje";
"overview.chargers.header.title" = "Resumen de cargadores";
"overview.chargers.empty.title" = "Aún no hay cargadores configurados";
"overview.chargers.empty.subtitle" = "Añade cargadores de toma de puerto, DC-DC o solares para conocer tu capacidad de carga.";
"overview.chargers.empty.create" = "Agregar cargador";
"battery.bank.badge.capacity" = "Capacidad";
"battery.bank.badge.energy" = "Energía";
"battery.bank.banner.voltage" = "Se detectó un desajuste de voltaje";

View File

@@ -175,6 +175,10 @@
"battery.onboarding.title" = "Ajoutez votre première batterie";
"battery.onboarding.subtitle" = "Suivez la capacité et la chimie de votre banc pour mieux maîtriser l'autonomie.";
"battery.bank.badge.voltage" = "Tension";
"overview.chargers.header.title" = "Vue densemble des chargeurs";
"overview.chargers.empty.title" = "Aucun chargeur configuré pour linstant";
"overview.chargers.empty.subtitle" = "Ajoutez des chargeurs secteur, DC-DC ou solaires pour comprendre votre capacité de charge.";
"overview.chargers.empty.create" = "Ajouter un chargeur";
"battery.bank.badge.capacity" = "Capacité";
"battery.bank.badge.energy" = "Énergie";
"battery.bank.banner.voltage" = "Écart de tension détecté";

View File

@@ -175,6 +175,10 @@
"battery.onboarding.title" = "Voeg je eerste accu toe";
"battery.onboarding.subtitle" = "Houd capaciteit en chemie van de accubank in de gaten om de gebruiksduur te beheersen.";
"battery.bank.badge.voltage" = "Spanning";
"overview.chargers.header.title" = "Overzicht van laders";
"overview.chargers.empty.title" = "Nog geen laders geconfigureerd";
"overview.chargers.empty.subtitle" = "Voeg walstroom-, DC-DC- of zonneladers toe om je laadvermogen te begrijpen.";
"overview.chargers.empty.create" = "Lader toevoegen";
"battery.bank.badge.capacity" = "Capaciteit";
"battery.bank.badge.energy" = "Energie";
"battery.bank.banner.voltage" = "Spanningsafwijking gedetecteerd";