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>
63 lines
1.7 KiB
Swift
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)
|
|
}
|
|
}
|