moodlit-native/Tests/UnitTests/HueColorTests.swift
till 9e9c607dc3 feat(smart-home): μ-9.0/.1/.2 Philips Hue Bridge — Setup + Mood-Push
Native-lokale Hue-Integration ohne Server-Side-Bridge — App-Key
bleibt im iOS-Keychain, kein mana-Service speichert Lampen-Auth.

Was:
- Sources/Core/SmartHome/HueClient.swift — CLIP v2 REST-Client mit
  Discovery (https://discovery.meethue.com), manueller IP-Probe,
  Press-Button-Pair, listLights, setLight (xy+brightness+on+
  transitionMs). Self-signed-Bridge-Cert via dedicated URLSession-
  Delegate akzeptiert (begründet im File).
- HueColor.swift — sRGB-Hex → CIE-1931-xy Konvertierung (Gamut C +
  D65) inkl. Gamma-Linearisierung. 7 Unit-Tests (Red/Blue/White/
  Black + Edge-Cases) grün.
- HueController.swift @Observable @MainActor:
  - SetupState-Maschine (notConfigured/pairing/configured/failed)
  - Bridge + Light-Selection in App-Group-UserDefaults, App-Key
    im iOS-Keychain (kSecAttrAccessibleAfterFirstUnlock)
  - applyMood(colors, brightness) verteilt Farben zyklisch über
    ausgewählte Lampen, transitionMs=600 für smooth Switch
  - turnOffSelected() beim Player-Close
- HueSettingsView — Discovery → Manueller-IP-Fallback → Pair
  (30s-Retry-Loop) → Light-Picker mit On-Status-Indikator
- SettingsView neuer "Smart Home"-Section mit NavigationLink zu HueSettingsView
- MoodPlayerView hookt onAppear/onChange-mood/onChange-isPaused/
  onDisappear in den HueController

Build iOS+macOS BUILD SUCCEEDED. 18/18 Tests grün (11 Domain + 7 Hue).

Was NICHT in dieser Phase:
- Entertainment-API (DTLS 50Hz) — kommt mit μ-10.3 Beat-Sync
- Animation-Effekte (pulse/breath/wave als Hue-Schleifen) — μ-9.2
- LIFX/Nanoleaf/WLED-Adapter — μ-9.5
- HomeKit-Output — μ-9.3
- Background-Bridge-Service — wenn überhaupt nötig

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

63 lines
1.7 KiB
Swift

import Foundation
import Testing
@testable import MoodlitNative
struct HueColorTests {
@Test func parsesHexWithHash() {
let rgb = HueColor.parseHex("#ff6b35")
#expect(rgb != nil)
#expect(abs((rgb?.r ?? 0) - 1.0) < 0.001)
#expect(abs((rgb?.g ?? 0) - 0x6b / 255.0) < 0.001)
}
@Test func parsesHexWithoutHash() {
let rgb = HueColor.parseHex("00ff00")
#expect(rgb != nil)
#expect(rgb?.g == 1.0)
#expect(rgb?.r == 0.0)
}
@Test func rejectsInvalidHex() {
#expect(HueColor.parseHex("nope") == nil)
#expect(HueColor.parseHex("#xyz123") == nil)
#expect(HueColor.parseHex("#ff6b") == nil) // zu kurz
}
@Test func pureRedConvertsToReasonableXY() {
// Pure rot in Gamut C x sollte in ~0.7-Region liegen
let result = HueColor.sRGBHexToXY("#ff0000")
#expect(result != nil)
let x = result!.xy.0
let y = result!.xy.1
#expect(x > 0.6 && x < 0.75)
#expect(y > 0.25 && y < 0.35)
#expect(result!.brightnessPct > 20)
}
@Test func pureBlueConvertsToReasonableXY() {
let result = HueColor.sRGBHexToXY("#0000ff")
#expect(result != nil)
let x = result!.xy.0
let y = result!.xy.1
#expect(x > 0.10 && x < 0.20)
#expect(y < 0.10)
}
@Test func pureWhiteIsNearD65() {
// sRGB-Weiß sollte nahe D65-Whitepoint landen (x=0.3127, y=0.3290)
let result = HueColor.sRGBHexToXY("#ffffff")
#expect(result != nil)
let x = result!.xy.0
let y = result!.xy.1
#expect(abs(x - 0.3127) < 0.02)
#expect(abs(y - 0.3290) < 0.02)
#expect(result!.brightnessPct == 100)
}
@Test func blackReturnsValidFallback() {
// Pure schwarz sum = 0, sollte D65-default zurückgeben + 0% Brightness
let result = HueColor.sRGBHexToXY("#000000")
#expect(result != nil)
#expect(result!.brightnessPct == 0)
}
}