Add color temp support for older HomeKit devices (#107206)

This commit is contained in:
J. Nick Koston 2024-01-04 22:50:26 -10:00 committed by GitHub
parent f0ec1235b1
commit 2641e4014a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 14 deletions

View File

@ -17,6 +17,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.color as color_util
from . import KNOWN_DEVICES from . import KNOWN_DEVICES
from .connection import HKDevice from .connection import HKDevice
@ -94,12 +95,16 @@ class HomeKitLight(HomeKitEntity, LightEntity):
@cached_property @cached_property
def min_mireds(self) -> int: def min_mireds(self) -> int:
"""Return minimum supported color temperature.""" """Return minimum supported color temperature."""
if not self.service.has(CharacteristicsTypes.COLOR_TEMPERATURE):
return super().min_mireds
min_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].minValue min_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].minValue
return int(min_value) if min_value else super().min_mireds return int(min_value) if min_value else super().min_mireds
@cached_property @cached_property
def max_mireds(self) -> int: def max_mireds(self) -> int:
"""Return the maximum color temperature.""" """Return the maximum color temperature."""
if not self.service.has(CharacteristicsTypes.COLOR_TEMPERATURE):
return super().max_mireds
max_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].maxValue max_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].maxValue
return int(max_value) if max_value else super().max_mireds return int(max_value) if max_value else super().max_mireds
@ -135,8 +140,9 @@ class HomeKitLight(HomeKitEntity, LightEntity):
CharacteristicsTypes.SATURATION CharacteristicsTypes.SATURATION
): ):
color_modes.add(ColorMode.HS) color_modes.add(ColorMode.HS)
color_modes.add(ColorMode.COLOR_TEMP)
if self.service.has(CharacteristicsTypes.COLOR_TEMPERATURE): elif self.service.has(CharacteristicsTypes.COLOR_TEMPERATURE):
color_modes.add(ColorMode.COLOR_TEMP) color_modes.add(ColorMode.COLOR_TEMP)
if not color_modes and self.service.has(CharacteristicsTypes.BRIGHTNESS): if not color_modes and self.service.has(CharacteristicsTypes.BRIGHTNESS):
@ -153,23 +159,36 @@ class HomeKitLight(HomeKitEntity, LightEntity):
temperature = kwargs.get(ATTR_COLOR_TEMP) temperature = kwargs.get(ATTR_COLOR_TEMP)
brightness = kwargs.get(ATTR_BRIGHTNESS) brightness = kwargs.get(ATTR_BRIGHTNESS)
characteristics = {} characteristics: dict[str, Any] = {}
if hs_color is not None:
characteristics.update(
{
CharacteristicsTypes.HUE: hs_color[0],
CharacteristicsTypes.SATURATION: hs_color[1],
}
)
if brightness is not None: if brightness is not None:
characteristics[CharacteristicsTypes.BRIGHTNESS] = int( characteristics[CharacteristicsTypes.BRIGHTNESS] = int(
brightness * 100 / 255 brightness * 100 / 255
) )
# If they send both temperature and hs_color, and the device
# does not support both, temperature will win. This is not
# expected to happen in the UI, but it is possible via a manual
# service call.
if temperature is not None: if temperature is not None:
characteristics[CharacteristicsTypes.COLOR_TEMPERATURE] = int(temperature) if self.service.has(CharacteristicsTypes.COLOR_TEMPERATURE):
characteristics[CharacteristicsTypes.COLOR_TEMPERATURE] = int(
temperature
)
elif hs_color is None:
# Some HomeKit devices implement color temperature with HS
# since the spec "technically" does not permit the COLOR_TEMPERATURE
# characteristic and the HUE and SATURATION characteristics to be
# present at the same time.
hue_sat = color_util.color_temperature_to_hs(
color_util.color_temperature_mired_to_kelvin(temperature)
)
characteristics[CharacteristicsTypes.HUE] = hue_sat[0]
characteristics[CharacteristicsTypes.SATURATION] = hue_sat[1]
if hs_color is not None:
characteristics[CharacteristicsTypes.HUE] = hs_color[0]
characteristics[CharacteristicsTypes.SATURATION] = hs_color[1]
characteristics[CharacteristicsTypes.ON] = True characteristics[CharacteristicsTypes.ON] = True

View File

@ -43,10 +43,17 @@
'attributes': dict({ 'attributes': dict({
'brightness': None, 'brightness': None,
'color_mode': None, 'color_mode': None,
'color_temp': None,
'color_temp_kelvin': None,
'friendly_name': 'Koogeek-LS1-20833F Light Strip', 'friendly_name': 'Koogeek-LS1-20833F Light Strip',
'hs_color': None, 'hs_color': None,
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'rgb_color': None, 'rgb_color': None,
'supported_color_modes': list([ 'supported_color_modes': list([
'color_temp',
'hs', 'hs',
]), ]),
'supported_features': 0, 'supported_features': 0,
@ -360,10 +367,17 @@
'attributes': dict({ 'attributes': dict({
'brightness': None, 'brightness': None,
'color_mode': None, 'color_mode': None,
'color_temp': None,
'color_temp_kelvin': None,
'friendly_name': 'Koogeek-LS1-20833F Light Strip', 'friendly_name': 'Koogeek-LS1-20833F Light Strip',
'hs_color': None, 'hs_color': None,
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'rgb_color': None, 'rgb_color': None,
'supported_color_modes': list([ 'supported_color_modes': list([
'color_temp',
'hs', 'hs',
]), ]),
'supported_features': 0, 'supported_features': 0,

View File

@ -1626,7 +1626,12 @@
]), ]),
'area_id': None, 'area_id': None,
'capabilities': dict({ 'capabilities': dict({
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'supported_color_modes': list([ 'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>, <ColorMode.HS: 'hs'>,
]), ]),
}), }),
@ -1656,10 +1661,17 @@
'attributes': dict({ 'attributes': dict({
'brightness': None, 'brightness': None,
'color_mode': None, 'color_mode': None,
'color_temp': None,
'color_temp_kelvin': None,
'friendly_name': 'Aqara Hub-1563 Lightbulb-1563', 'friendly_name': 'Aqara Hub-1563 Lightbulb-1563',
'hs_color': None, 'hs_color': None,
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'rgb_color': None, 'rgb_color': None,
'supported_color_modes': list([ 'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>, <ColorMode.HS: 'hs'>,
]), ]),
'supported_features': <LightEntityFeature: 0>, 'supported_features': <LightEntityFeature: 0>,
@ -2014,7 +2026,12 @@
]), ]),
'area_id': None, 'area_id': None,
'capabilities': dict({ 'capabilities': dict({
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'supported_color_modes': list([ 'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>, <ColorMode.HS: 'hs'>,
]), ]),
}), }),
@ -2044,10 +2061,17 @@
'attributes': dict({ 'attributes': dict({
'brightness': None, 'brightness': None,
'color_mode': None, 'color_mode': None,
'color_temp': None,
'color_temp_kelvin': None,
'friendly_name': 'ArloBabyA0 Nightlight', 'friendly_name': 'ArloBabyA0 Nightlight',
'hs_color': None, 'hs_color': None,
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'rgb_color': None, 'rgb_color': None,
'supported_color_modes': list([ 'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>, <ColorMode.HS: 'hs'>,
]), ]),
'supported_features': <LightEntityFeature: 0>, 'supported_features': <LightEntityFeature: 0>,
@ -9279,7 +9303,12 @@
]), ]),
'area_id': None, 'area_id': None,
'capabilities': dict({ 'capabilities': dict({
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'supported_color_modes': list([ 'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>, <ColorMode.HS: 'hs'>,
]), ]),
}), }),
@ -9309,10 +9338,17 @@
'attributes': dict({ 'attributes': dict({
'brightness': None, 'brightness': None,
'color_mode': None, 'color_mode': None,
'color_temp': None,
'color_temp_kelvin': None,
'friendly_name': 'Laundry Smoke ED78', 'friendly_name': 'Laundry Smoke ED78',
'hs_color': None, 'hs_color': None,
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'rgb_color': None, 'rgb_color': None,
'supported_color_modes': list([ 'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>, <ColorMode.HS: 'hs'>,
]), ]),
'supported_features': <LightEntityFeature: 0>, 'supported_features': <LightEntityFeature: 0>,
@ -11535,7 +11571,12 @@
]), ]),
'area_id': None, 'area_id': None,
'capabilities': dict({ 'capabilities': dict({
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'supported_color_modes': list([ 'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>, <ColorMode.HS: 'hs'>,
]), ]),
}), }),
@ -11565,10 +11606,17 @@
'attributes': dict({ 'attributes': dict({
'brightness': None, 'brightness': None,
'color_mode': None, 'color_mode': None,
'color_temp': None,
'color_temp_kelvin': None,
'friendly_name': 'Koogeek-LS1-20833F Light Strip', 'friendly_name': 'Koogeek-LS1-20833F Light Strip',
'hs_color': None, 'hs_color': None,
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'rgb_color': None, 'rgb_color': None,
'supported_color_modes': list([ 'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>, <ColorMode.HS: 'hs'>,
]), ]),
'supported_features': <LightEntityFeature: 0>, 'supported_features': <LightEntityFeature: 0>,
@ -16318,7 +16366,12 @@
]), ]),
'area_id': None, 'area_id': None,
'capabilities': dict({ 'capabilities': dict({
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'supported_color_modes': list([ 'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>, <ColorMode.HS: 'hs'>,
]), ]),
}), }),
@ -16348,17 +16401,24 @@
'attributes': dict({ 'attributes': dict({
'brightness': 127.5, 'brightness': 127.5,
'color_mode': <ColorMode.HS: 'hs'>, 'color_mode': <ColorMode.HS: 'hs'>,
'color_temp': None,
'color_temp_kelvin': None,
'friendly_name': 'VOCOlinc-Flowerbud-0d324b Mood Light', 'friendly_name': 'VOCOlinc-Flowerbud-0d324b Mood Light',
'hs_color': tuple( 'hs_color': tuple(
120.0, 120.0,
100.0, 100.0,
), ),
'max_color_temp_kelvin': 6535,
'max_mireds': 500,
'min_color_temp_kelvin': 2000,
'min_mireds': 153,
'rgb_color': tuple( 'rgb_color': tuple(
0, 0,
255, 255,
0, 0,
), ),
'supported_color_modes': list([ 'supported_color_modes': list([
<ColorMode.COLOR_TEMP: 'color_temp'>,
<ColorMode.HS: 'hs'>, <ColorMode.HS: 'hs'>,
]), ]),
'supported_features': <LightEntityFeature: 0>, 'supported_features': <LightEntityFeature: 0>,

View File

@ -39,4 +39,7 @@ async def test_light_add_feature_at_runtime(
await device_config_changed(hass, accessories) await device_config_changed(hass, accessories)
light_state = hass.states.get("light.laundry_smoke_ed78") light_state = hass.states.get("light.laundry_smoke_ed78")
assert light_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.HS] assert light_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]

View File

@ -74,6 +74,22 @@ async def test_switch_change_light_state(hass: HomeAssistant) -> None:
}, },
) )
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": "light.testdevice", "brightness": 255, "color_temp": 300},
blocking=True,
)
helper.async_assert_service_values(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
CharacteristicsTypes.HUE: 27,
CharacteristicsTypes.SATURATION: 49,
},
)
await hass.services.async_call( await hass.services.async_call(
"light", "turn_off", {"entity_id": "light.testdevice"}, blocking=True "light", "turn_off", {"entity_id": "light.testdevice"}, blocking=True
) )
@ -176,7 +192,10 @@ async def test_switch_read_light_state_hs(hass: HomeAssistant) -> None:
state = await helper.poll_and_get_state() state = await helper.poll_and_get_state()
assert state.state == "off" assert state.state == "off"
assert state.attributes[ATTR_COLOR_MODE] is None assert state.attributes[ATTR_COLOR_MODE] is None
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.HS] assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
# Simulate that someone switched on the device in the real world not via HA # Simulate that someone switched on the device in the real world not via HA
@ -193,7 +212,10 @@ async def test_switch_read_light_state_hs(hass: HomeAssistant) -> None:
assert state.attributes["brightness"] == 255 assert state.attributes["brightness"] == 255
assert state.attributes["hs_color"] == (4, 5) assert state.attributes["hs_color"] == (4, 5)
assert state.attributes[ATTR_COLOR_MODE] == ColorMode.HS assert state.attributes[ATTR_COLOR_MODE] == ColorMode.HS
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.HS] assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
# Simulate that device switched off in the real world not via HA # Simulate that device switched off in the real world not via HA
@ -205,6 +227,25 @@ async def test_switch_read_light_state_hs(hass: HomeAssistant) -> None:
) )
assert state.state == "off" assert state.state == "off"
# Simulate that device switched on in the real world not via HA
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.HUE: 6,
CharacteristicsTypes.SATURATION: 7,
},
)
assert state.state == "on"
assert state.attributes["brightness"] == 255
assert state.attributes["hs_color"] == (6, 7)
assert state.attributes[ATTR_COLOR_MODE] == ColorMode.HS
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
async def test_switch_push_light_state_hs(hass: HomeAssistant) -> None: async def test_switch_push_light_state_hs(hass: HomeAssistant) -> None:
"""Test that we can read the state of a HomeKit light accessory.""" """Test that we can read the state of a HomeKit light accessory."""