moodlit-native/Sources/Features/Settings/HomeKitSettingsView.swift
till b887917d81 feat(smart-home): μ-9.3 HomeKit als zweiter Smart-Home-Treiber
iOS-only: Apple Home-Lampen reagieren auf Mood-Wechsel parallel
zur direkten Hue-Bridge. Trade-off: HomeKit rate-limited (~10Hz),
für Mood-Switches okay, für Beat-Sync zu langsam.

Was:
- Sources/Core/SmartHome/HomeKitController.swift @Observable +
  HMHomeManagerDelegate:
  - start(): triggert iOS-HomeKit-Permission-Prompt
  - LampAccessory-Indexer extrahiert nur Lightbulb-Services, gruppiert
    nach Room
  - applyMood schreibt Hue/Saturation/Brightness/PowerState pro
    Accessory mit RGB→HSV-Konvertierung (HomeKit-Skala: h 0-360,
    s/v 0-100)
  - Selected-Lamps in App-Group-UserDefaults
  - turnOffSelected() beim Player-Close
- Sources/Features/Settings/HomeKitSettingsView.swift: Permission-State-
  Anzeige + Lamps gruppiert nach Room mit Multi-Select-Toggle
- SettingsView: NavigationLink zu HomeKitSettingsView (iOS-only)
- MoodPlayerView: pushToHomeKit() + turnOffHomeLights() parallel zu Hue
- project.yml: NSHomeKitUsageDescription + NSLocalNetworkUsageDescription
  in Info.plist, com.apple.developer.homekit-Entitlement
  (Apple-Dev-Portal-Capability-Aktivierung steht noch aus)

primaryHome → homes.first weil iOS-16.1-Deprecation.

Build iOS+macOS grün, 18/18 Tests grün.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:18:50 +02:00

121 lines
3.2 KiB
Swift

#if os(iOS)
import SwiftUI
/// Setup-UI für HomeKit-Lampen. Permission-Request Room-/Lamp-Liste
/// Per-Lamp-Toggle. Persistiert in App-Group-UserDefaults.
public struct HomeKitSettingsView: View {
@Environment(HomeKitController.self) private var controller
public init() {}
public var body: some View {
Form {
authSection
if controller.authState == .authorized {
if controller.availableLamps.isEmpty {
emptyHomeSection
} else {
lampsSection
}
}
infoSection
}
.navigationTitle("Apple Home")
.navigationBarTitleDisplayMode(.inline)
.onAppear {
controller.start()
}
}
@ViewBuilder
private var authSection: some View {
Section {
switch controller.authState {
case .notRequested:
Label("Bereit", systemImage: "house")
case .requesting:
HStack(spacing: 12) {
ProgressView()
Text("Lade HomeKit …")
}
case .authorized:
Label("HomeKit erlaubt", systemImage: "checkmark.circle.fill")
.foregroundStyle(.green)
case .denied:
VStack(alignment: .leading, spacing: 6) {
Label("HomeKit-Zugriff verweigert", systemImage: "lock")
.foregroundStyle(.orange)
Text("Aktiviere in den iPhone-Einstellungen → Datenschutz → HomeKit → Moodlit.")
.font(.caption)
.foregroundStyle(MoodlitTheme.mutedForeground)
}
case .restricted:
Label(
"HomeKit ist auf diesem Gerät systemseitig eingeschränkt.",
systemImage: "exclamationmark.triangle"
)
.foregroundStyle(.orange)
}
}
}
@ViewBuilder
private var emptyHomeSection: some View {
Section {
VStack(alignment: .leading, spacing: 6) {
Text("Kein HomeKit-Zuhause oder keine kompatiblen Lampen gefunden.")
.font(.body)
Text("Lege in der Home-App ein Zuhause an und füge Lampen hinzu. Sie erscheinen dann hier.")
.font(.caption)
.foregroundStyle(MoodlitTheme.mutedForeground)
}
}
}
@ViewBuilder
private var lampsSection: some View {
let grouped = Dictionary(grouping: controller.availableLamps, by: \.room)
ForEach(grouped.keys.sorted { ($0 ?? "z") < ($1 ?? "z") }, id: \.self) { room in
Section(room ?? "Ohne Raum") {
ForEach(grouped[room] ?? []) { lamp in
Button {
controller.toggleSelected(lampId: lamp.id)
} label: {
HStack(spacing: 12) {
Image(systemName: controller.selectedLampIds.contains(lamp.id)
? "checkmark.circle.fill"
: "circle"
)
.foregroundStyle(
controller.selectedLampIds.contains(lamp.id)
? MoodlitTheme.primary
: MoodlitTheme.mutedForeground
)
VStack(alignment: .leading, spacing: 2) {
Text(lamp.name)
if !lamp.supportsColor {
Text("nur Helligkeit")
.font(.caption2)
.foregroundStyle(MoodlitTheme.mutedForeground)
}
}
Spacer()
}
}
.buttonStyle(.plain)
.contentShape(Rectangle())
}
}
}
}
@ViewBuilder
private var infoSection: some View {
Section {
Text("HomeKit ist langsamer als die direkte Hue-Anbindung (max ~10 Updates pro Sekunde pro Lampe). Für Mood-Wechsel okay, für Beat-Sync zu langsam — dann lieber direkten Hue-Bridge-Pfad nutzen.")
.font(.caption)
.foregroundStyle(MoodlitTheme.mutedForeground)
}
}
}
#endif