Overview absorbs Observed Metrics: current/power/voltage extremes appear as compact 2-col pairs after a divider inside the same card. Battery absorbs Energy: Battery Energy + Capacity Estimate are the first row of the Battery card (side by side), followed by secondary energy rows, then battery percentages (paired), then checkpoints. Removes the standalone Energy and Observed Metrics cards and their call-sites. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -197,8 +197,6 @@ struct ChargeSessionDetailView: View {
|
||
| 197 | 197 |
} |
| 198 | 198 |
} else {
|
| 199 | 199 |
overviewCard(session, chargedDevice: chargedDevice) |
| 200 |
- energyCard(session, chargedDevice: chargedDevice) |
|
| 201 |
- observedMetricsCard(session, chargedDevice: chargedDevice) |
|
| 202 | 200 |
batteryCard(session, chargedDevice: chargedDevice) |
| 203 | 201 |
|
| 204 | 202 |
if shouldShowSessionChart(session) {
|
@@ -396,6 +394,55 @@ struct ChargeSessionDetailView: View {
|
||
| 396 | 394 |
} |
| 397 | 395 |
} |
| 398 | 396 |
} |
| 397 |
+ |
|
| 398 |
+ if session.minimumObservedCurrentAmps != nil |
|
| 399 |
+ || session.maximumObservedCurrentAmps != nil |
|
| 400 |
+ || session.maximumObservedPowerWatts != nil |
|
| 401 |
+ || session.maximumObservedVoltageVolts != nil |
|
| 402 |
+ || (chargedDevice.isCharger && session.selectedSourceVoltageVolts != nil) |
|
| 403 |
+ || session.completionCurrentAmps != nil |
|
| 404 |
+ || session.selectedDataGroup != nil {
|
|
| 405 |
+ |
|
| 406 |
+ Divider() |
|
| 407 |
+ |
|
| 408 |
+ if session.minimumObservedCurrentAmps != nil || session.maximumObservedCurrentAmps != nil {
|
|
| 409 |
+ HStack(alignment: .top, spacing: 12) {
|
|
| 410 |
+ if let v = session.minimumObservedCurrentAmps {
|
|
| 411 |
+ overviewStatCell(label: "Min Current", value: "\(v.format(decimalDigits: 2)) A") |
|
| 412 |
+ } |
|
| 413 |
+ if let v = session.maximumObservedCurrentAmps {
|
|
| 414 |
+ overviewStatCell(label: "Max Current", value: "\(v.format(decimalDigits: 2)) A") |
|
| 415 |
+ } |
|
| 416 |
+ } |
|
| 417 |
+ } |
|
| 418 |
+ |
|
| 419 |
+ if session.maximumObservedPowerWatts != nil || session.maximumObservedVoltageVolts != nil {
|
|
| 420 |
+ HStack(alignment: .top, spacing: 12) {
|
|
| 421 |
+ if let v = session.maximumObservedPowerWatts {
|
|
| 422 |
+ overviewStatCell(label: "Max Power", value: "\(v.format(decimalDigits: 2)) W") |
|
| 423 |
+ } |
|
| 424 |
+ if let v = session.maximumObservedVoltageVolts {
|
|
| 425 |
+ overviewStatCell(label: "Max Voltage", value: "\(v.format(decimalDigits: 2)) V") |
|
| 426 |
+ } |
|
| 427 |
+ } |
|
| 428 |
+ } |
|
| 429 |
+ |
|
| 430 |
+ if session.completionCurrentAmps != nil |
|
| 431 |
+ || (chargedDevice.isCharger && session.selectedSourceVoltageVolts != nil) {
|
|
| 432 |
+ HStack(alignment: .top, spacing: 12) {
|
|
| 433 |
+ if let v = session.completionCurrentAmps {
|
|
| 434 |
+ overviewStatCell(label: "Completion Current", value: "\(v.format(decimalDigits: 2)) A") |
|
| 435 |
+ } |
|
| 436 |
+ if chargedDevice.isCharger, let v = session.selectedSourceVoltageVolts {
|
|
| 437 |
+ overviewStatCell(label: "Selected Voltage", value: "\(v.format(decimalDigits: 2)) V") |
|
| 438 |
+ } |
|
| 439 |
+ } |
|
| 440 |
+ } |
|
| 441 |
+ |
|
| 442 |
+ if let dg = session.selectedDataGroup {
|
|
| 443 |
+ MeterInfoRowView(label: "Data Group", value: "\(dg)") |
|
| 444 |
+ } |
|
| 445 |
+ } |
|
| 399 | 446 |
} |
| 400 | 447 |
} |
| 401 | 448 |
} |
@@ -412,70 +459,6 @@ struct ChargeSessionDetailView: View {
|
||
| 412 | 459 |
.frame(maxWidth: .infinity, alignment: .leading) |
| 413 | 460 |
} |
| 414 | 461 |
|
| 415 |
- private func energyCard( |
|
| 416 |
- _ session: ChargeSessionSummary, |
|
| 417 |
- chargedDevice: ChargedDeviceSummary |
|
| 418 |
- ) -> some View {
|
|
| 419 |
- let displayedEnergyWh = displayedSessionEnergyWh(for: session) |
|
| 420 |
- |
|
| 421 |
- return MeterInfoCardView(title: "Energy", tint: .teal, isCollapsible: true) {
|
|
| 422 |
- MeterInfoRowView(label: "Battery Energy", value: "\(displayedEnergyWh.format(decimalDigits: 3)) Wh") |
|
| 423 |
- if abs(displayedEnergyWh - session.measuredEnergyWh) > 0.01 {
|
|
| 424 |
- MeterInfoRowView(label: "Measured Energy", value: "\(session.measuredEnergyWh.format(decimalDigits: 3)) Wh") |
|
| 425 |
- } |
|
| 426 |
- if let effectiveBatteryEnergyWh = session.effectiveBatteryEnergyWh, |
|
| 427 |
- abs(effectiveBatteryEnergyWh - session.measuredEnergyWh) > 0.01 {
|
|
| 428 |
- MeterInfoRowView(label: "Effective Battery Energy", value: "\(effectiveBatteryEnergyWh.format(decimalDigits: 3)) Wh") |
|
| 429 |
- } |
|
| 430 |
- if let capacityEstimateWh = session.capacityEstimateWh {
|
|
| 431 |
- MeterInfoRowView(label: "Capacity Estimate", value: "\(capacityEstimateWh.format(decimalDigits: 2)) Wh") |
|
| 432 |
- } |
|
| 433 |
- if let chargerID = session.chargerID, |
|
| 434 |
- let charger = appData.chargedDeviceSummary(id: chargerID) {
|
|
| 435 |
- MeterInfoRowView(label: chargedDevice.hasMultipleChargingTransports ? "Wireless Charger" : "Charger", value: charger.name) |
|
| 436 |
- } |
|
| 437 |
- if let wirelessSessionHint = wirelessSessionHint(for: session) {
|
|
| 438 |
- Text(wirelessSessionHint) |
|
| 439 |
- .font(.caption2) |
|
| 440 |
- .foregroundColor(session.shouldWarnAboutLowWirelessEfficiency ? .orange : .secondary) |
|
| 441 |
- } |
|
| 442 |
- if let sessionWarning = sessionWarning(for: session) {
|
|
| 443 |
- Label(sessionWarning, systemImage: "exclamationmark.triangle") |
|
| 444 |
- .font(.caption2) |
|
| 445 |
- .foregroundColor(.orange) |
|
| 446 |
- } |
|
| 447 |
- } |
|
| 448 |
- } |
|
| 449 |
- |
|
| 450 |
- private func observedMetricsCard( |
|
| 451 |
- _ session: ChargeSessionSummary, |
|
| 452 |
- chargedDevice: ChargedDeviceSummary |
|
| 453 |
- ) -> some View {
|
|
| 454 |
- MeterInfoCardView(title: "Observed Metrics", tint: .blue, isCollapsible: true, initiallyExpanded: false) {
|
|
| 455 |
- if let minimumObservedCurrentAmps = session.minimumObservedCurrentAmps {
|
|
| 456 |
- MeterInfoRowView(label: "Min Current", value: "\(minimumObservedCurrentAmps.format(decimalDigits: 2)) A") |
|
| 457 |
- } |
|
| 458 |
- if let maximumObservedCurrentAmps = session.maximumObservedCurrentAmps {
|
|
| 459 |
- MeterInfoRowView(label: "Max Current", value: "\(maximumObservedCurrentAmps.format(decimalDigits: 2)) A") |
|
| 460 |
- } |
|
| 461 |
- if let maximumObservedPowerWatts = session.maximumObservedPowerWatts {
|
|
| 462 |
- MeterInfoRowView(label: "Max Power", value: "\(maximumObservedPowerWatts.format(decimalDigits: 2)) W") |
|
| 463 |
- } |
|
| 464 |
- if let maximumObservedVoltageVolts = session.maximumObservedVoltageVolts {
|
|
| 465 |
- MeterInfoRowView(label: "Max Voltage", value: "\(maximumObservedVoltageVolts.format(decimalDigits: 2)) V") |
|
| 466 |
- } |
|
| 467 |
- if chargedDevice.isCharger, let selectedSourceVoltageVolts = session.selectedSourceVoltageVolts {
|
|
| 468 |
- MeterInfoRowView(label: "Selected Voltage", value: "\(selectedSourceVoltageVolts.format(decimalDigits: 2)) V") |
|
| 469 |
- } |
|
| 470 |
- if let completionCurrentAmps = session.completionCurrentAmps {
|
|
| 471 |
- MeterInfoRowView(label: "Completion Current", value: "\(completionCurrentAmps.format(decimalDigits: 2)) A") |
|
| 472 |
- } |
|
| 473 |
- if session.selectedDataGroup != nil {
|
|
| 474 |
- MeterInfoRowView(label: "Data Group", value: "\(session.selectedDataGroup ?? 0)") |
|
| 475 |
- } |
|
| 476 |
- } |
|
| 477 |
- } |
|
| 478 |
- |
|
| 479 | 462 |
private func batteryCard( |
| 480 | 463 |
_ session: ChargeSessionSummary, |
| 481 | 464 |
chargedDevice: ChargedDeviceSummary |
@@ -487,44 +470,87 @@ struct ChargeSessionDetailView: View {
|
||
| 487 | 470 |
) |
| 488 | 471 |
|
| 489 | 472 |
return MeterInfoCardView(title: "Battery", tint: .orange, isCollapsible: true) {
|
| 490 |
- if let startBatteryPercent = session.startBatteryPercent {
|
|
| 491 |
- MeterInfoRowView(label: "Start Battery", value: "\(startBatteryPercent.format(decimalDigits: 0))%") |
|
| 492 |
- } |
|
| 493 |
- if let endBatteryPercent = session.endBatteryPercent {
|
|
| 494 |
- MeterInfoRowView(label: "End Battery", value: "\(endBatteryPercent.format(decimalDigits: 0))%") |
|
| 495 |
- } |
|
| 496 |
- if let batteryDeltaPercent = session.batteryDeltaPercent {
|
|
| 497 |
- MeterInfoRowView(label: "Battery Delta", value: "\(batteryDeltaPercent.format(decimalDigits: 0))%") |
|
| 498 |
- } |
|
| 499 |
- if let targetBatteryPercent = session.targetBatteryPercent {
|
|
| 500 |
- MeterInfoRowView(label: "Target Notification", value: "\(targetBatteryPercent.format(decimalDigits: 0))%") |
|
| 501 |
- } |
|
| 502 |
- if let batteryPrediction {
|
|
| 503 |
- MeterInfoRowView( |
|
| 504 |
- label: "Predicted Battery", |
|
| 505 |
- value: "\(batteryPrediction.predictedPercent.format(decimalDigits: 0))%" |
|
| 506 |
- ) |
|
| 507 |
- Text( |
|
| 508 |
- "Anchored to \(batteryPrediction.anchorDescription) at \(batteryPrediction.anchorPercent.format(decimalDigits: 0))% using \(batteryPrediction.estimatedCapacityWh.format(decimalDigits: 2)) Wh estimated capacity." |
|
| 509 |
- ) |
|
| 510 |
- .font(.caption2) |
|
| 511 |
- .foregroundColor(.secondary) |
|
| 512 |
- } |
|
| 473 |
+ VStack(alignment: .leading, spacing: 10) {
|
|
| 513 | 474 |
|
| 514 |
- BatteryCheckpointSectionView( |
|
| 515 |
- sessionID: session.id, |
|
| 516 |
- checkpoints: session.checkpoints, |
|
| 517 |
- message: session.status.isOpen |
|
| 518 |
- ? "The checkpoint is stored on the active charge session and is used for capacity estimation and charge-level prediction." |
|
| 519 |
- : "These checkpoints were saved with this closed session and feed capacity estimation and charge-level prediction.", |
|
| 520 |
- canAddCheckpoint: hasMonitoringControls && appData.canAddBatteryCheckpoint(to: session.id), |
|
| 521 |
- canDeleteCheckpoint: hasMonitoringControls || session.status.isOpen == false, |
|
| 522 |
- requirementMessage: session.status.isOpen ? appData.batteryCheckpointCaptureRequirementMessage(for: session.id) : nil, |
|
| 523 |
- effectiveEnergyWhOverride: hasMonitoringControls ? displayedEnergyWh : nil, |
|
| 524 |
- onDelete: { checkpoint in
|
|
| 525 |
- pendingCheckpointDeletion = checkpoint |
|
| 475 |
+ // Energy |
|
| 476 |
+ HStack(alignment: .top, spacing: 12) {
|
|
| 477 |
+ overviewStatCell(label: "Battery Energy", value: "\(displayedEnergyWh.format(decimalDigits: 3)) Wh") |
|
| 478 |
+ if let capacityEstimateWh = session.capacityEstimateWh {
|
|
| 479 |
+ overviewStatCell(label: "Capacity Estimate", value: "\(capacityEstimateWh.format(decimalDigits: 2)) Wh") |
|
| 480 |
+ } |
|
| 526 | 481 |
} |
| 527 |
- ) |
|
| 482 |
+ if abs(displayedEnergyWh - session.measuredEnergyWh) > 0.01 {
|
|
| 483 |
+ MeterInfoRowView(label: "Measured Energy", value: "\(session.measuredEnergyWh.format(decimalDigits: 3)) Wh") |
|
| 484 |
+ } |
|
| 485 |
+ if let effectiveBatteryEnergyWh = session.effectiveBatteryEnergyWh, |
|
| 486 |
+ abs(effectiveBatteryEnergyWh - session.measuredEnergyWh) > 0.01 {
|
|
| 487 |
+ MeterInfoRowView(label: "Effective Battery Energy", value: "\(effectiveBatteryEnergyWh.format(decimalDigits: 3)) Wh") |
|
| 488 |
+ } |
|
| 489 |
+ if let chargerID = session.chargerID, |
|
| 490 |
+ let charger = appData.chargedDeviceSummary(id: chargerID) {
|
|
| 491 |
+ MeterInfoRowView(label: chargedDevice.hasMultipleChargingTransports ? "Wireless Charger" : "Charger", value: charger.name) |
|
| 492 |
+ } |
|
| 493 |
+ if let wirelessSessionHint = wirelessSessionHint(for: session) {
|
|
| 494 |
+ Text(wirelessSessionHint) |
|
| 495 |
+ .font(.caption2) |
|
| 496 |
+ .foregroundColor(session.shouldWarnAboutLowWirelessEfficiency ? .orange : .secondary) |
|
| 497 |
+ } |
|
| 498 |
+ if let sessionWarning = sessionWarning(for: session) {
|
|
| 499 |
+ Label(sessionWarning, systemImage: "exclamationmark.triangle") |
|
| 500 |
+ .font(.caption2) |
|
| 501 |
+ .foregroundColor(.orange) |
|
| 502 |
+ } |
|
| 503 |
+ |
|
| 504 |
+ // Battery percentages |
|
| 505 |
+ if session.startBatteryPercent != nil || session.endBatteryPercent != nil {
|
|
| 506 |
+ Divider() |
|
| 507 |
+ HStack(alignment: .top, spacing: 12) {
|
|
| 508 |
+ if let v = session.startBatteryPercent {
|
|
| 509 |
+ overviewStatCell(label: "Start Battery", value: "\(v.format(decimalDigits: 0))%") |
|
| 510 |
+ } |
|
| 511 |
+ if let v = session.endBatteryPercent {
|
|
| 512 |
+ overviewStatCell(label: "End Battery", value: "\(v.format(decimalDigits: 0))%") |
|
| 513 |
+ } |
|
| 514 |
+ } |
|
| 515 |
+ if session.batteryDeltaPercent != nil || session.targetBatteryPercent != nil {
|
|
| 516 |
+ HStack(alignment: .top, spacing: 12) {
|
|
| 517 |
+ if let v = session.batteryDeltaPercent {
|
|
| 518 |
+ overviewStatCell(label: "Battery Delta", value: "\(v.format(decimalDigits: 0))%") |
|
| 519 |
+ } |
|
| 520 |
+ if let v = session.targetBatteryPercent {
|
|
| 521 |
+ overviewStatCell(label: "Target", value: "\(v.format(decimalDigits: 0))%") |
|
| 522 |
+ } |
|
| 523 |
+ } |
|
| 524 |
+ } |
|
| 525 |
+ if let batteryPrediction {
|
|
| 526 |
+ HStack(alignment: .top, spacing: 12) {
|
|
| 527 |
+ overviewStatCell(label: "Predicted Battery", value: "\(batteryPrediction.predictedPercent.format(decimalDigits: 0))%") |
|
| 528 |
+ } |
|
| 529 |
+ Text( |
|
| 530 |
+ "Anchored to \(batteryPrediction.anchorDescription) at \(batteryPrediction.anchorPercent.format(decimalDigits: 0))% using \(batteryPrediction.estimatedCapacityWh.format(decimalDigits: 2)) Wh estimated capacity." |
|
| 531 |
+ ) |
|
| 532 |
+ .font(.caption2) |
|
| 533 |
+ .foregroundColor(.secondary) |
|
| 534 |
+ } |
|
| 535 |
+ } |
|
| 536 |
+ |
|
| 537 |
+ // Checkpoints |
|
| 538 |
+ Divider() |
|
| 539 |
+ BatteryCheckpointSectionView( |
|
| 540 |
+ sessionID: session.id, |
|
| 541 |
+ checkpoints: session.checkpoints, |
|
| 542 |
+ message: session.status.isOpen |
|
| 543 |
+ ? "The checkpoint is stored on the active charge session and is used for capacity estimation and charge-level prediction." |
|
| 544 |
+ : "These checkpoints were saved with this closed session and feed capacity estimation and charge-level prediction.", |
|
| 545 |
+ canAddCheckpoint: hasMonitoringControls && appData.canAddBatteryCheckpoint(to: session.id), |
|
| 546 |
+ canDeleteCheckpoint: hasMonitoringControls || session.status.isOpen == false, |
|
| 547 |
+ requirementMessage: session.status.isOpen ? appData.batteryCheckpointCaptureRequirementMessage(for: session.id) : nil, |
|
| 548 |
+ effectiveEnergyWhOverride: hasMonitoringControls ? displayedEnergyWh : nil, |
|
| 549 |
+ onDelete: { checkpoint in
|
|
| 550 |
+ pendingCheckpointDeletion = checkpoint |
|
| 551 |
+ } |
|
| 552 |
+ ) |
|
| 553 |
+ } |
|
| 528 | 554 |
} |
| 529 | 555 |
} |
| 530 | 556 |
|