Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Hemera/Entities/Light/LightController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ final class LightController: LightControlling, ServiceCallErrorHandling {

func setColorTemp(_ id: String, to mireds: Int) async {
await performServiceCall("Failed to set color temp for \(id)") {
let kelvin = mireds > 0 ? 1_000_000 / mireds : 0
try await serviceCaller.callService(
domain: Keys.domain,
service: Keys.turnOn,
entityId: id,
extraData: [Keys.colorTemp: mireds]
extraData: [Keys.colorTempKelvin: kelvin]
)
}
}
Expand All @@ -61,6 +62,6 @@ private enum Keys {
static let turnOn = "turn_on"
static let turnOff = "turn_off"
static let brightness = "brightness"
static let colorTemp = "color_temp"
static let colorTempKelvin = "color_temp_kelvin"
static let hsColor = "hs_color"
}
17 changes: 17 additions & 0 deletions Hemera/Entities/Light/LightEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,17 @@ final class LightEntity: StoredEntity {
brightness = entity.attributes["brightness"] as? Int
colorMode = entity.attributes["color_mode"] as? String
colorTemp = entity.attributes["color_temp"] as? Int
?? Self.miredsFromKelvin(entity.attributes["color_temp_kelvin"])
if let hs = entity.attributes["hs_color"] as? [Any] {
hsColor = hs.compactMap { ($0 as? NSNumber)?.doubleValue }
if hsColor?.count != 2 { hsColor = nil }
} else {
hsColor = nil
}
let rawMinMireds = entity.attributes["min_mireds"] as? Int
?? Self.miredsFromKelvin(entity.attributes["max_color_temp_kelvin"])
let rawMaxMireds = entity.attributes["max_mireds"] as? Int
?? Self.miredsFromKelvin(entity.attributes["min_color_temp_kelvin"])
if let lo = rawMinMireds, let hi = rawMaxMireds {
minMireds = Swift.min(lo, hi)
maxMireds = Swift.max(lo, hi)
Expand All @@ -101,6 +104,20 @@ final class LightEntity: StoredEntity {
}
}

// MARK: - Kelvin conversion

private extension LightEntity {
/**
Converts a HA `*_color_temp_kelvin` attribute to mireds (their reciprocal).
Returns nil when the attribute is absent or non-positive so the caller can
fall back to the legacy mired attributes.
*/
static func miredsFromKelvin(_ value: Any?) -> Int? {
guard let kelvin = value as? Int, kelvin > 0 else { return nil }
return 1_000_000 / kelvin
}
}

// MARK: - StoredEntity

extension LightEntity {
Expand Down
36 changes: 36 additions & 0 deletions HemeraTests/Entities/LightEntityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,40 @@ struct LightEntityTests {
#expect(entity.minMireds == nil)
#expect(entity.maxMireds == nil)
}

@Test
func update_withKelvinAttributes_derivesInvertedMireds() throws {
let entity = LightEntity(entityId: "light.test")
// HA 2026.3 drops mireds; only kelvin bounds are sent. min mired ↔ max kelvin.
let haEntity = try makeHAEntity(attributes: [
"color_temp_kelvin": 4000,
"min_color_temp_kelvin": 2000,
"max_color_temp_kelvin": 6535
])

entity.update(from: haEntity)

#expect(entity.colorTemp == 1_000_000 / 4000) // 250
#expect(entity.minMireds == 1_000_000 / 6535) // 153, from the highest kelvin
#expect(entity.maxMireds == 1_000_000 / 2000) // 500, from the lowest kelvin
}

@Test
func update_prefersLegacyMiredsOverKelvin() throws {
let entity = LightEntity(entityId: "light.test")
let haEntity = try makeHAEntity(attributes: [
"color_temp": 300,
"min_mireds": 153,
"max_mireds": 500,
"color_temp_kelvin": 4000,
"min_color_temp_kelvin": 2000,
"max_color_temp_kelvin": 6535
])

entity.update(from: haEntity)

#expect(entity.colorTemp == 300)
#expect(entity.minMireds == 153)
#expect(entity.maxMireds == 500)
}
}
Loading