core/tests/components/homekit_controller/test_light.py

478 lines
15 KiB
Python

"""Basic checks for HomeKitSwitch."""
from collections.abc import Callable
from unittest import mock
from aiohomekit.model import Accessory
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import Service, ServicesTypes
from aiohomekit.testing import FakeController
from homeassistant.components.homekit_controller.const import KNOWN_DEVICES
from homeassistant.components.light import (
ATTR_COLOR_MODE,
ATTR_SUPPORTED_COLOR_MODES,
ColorMode,
)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .common import setup_test_component
LIGHT_BULB_NAME = "TestDevice"
LIGHT_BULB_ENTITY_ID = "light.testdevice"
def create_lightbulb_service(accessory: Accessory) -> Service:
"""Define lightbulb characteristics."""
service = accessory.add_service(ServicesTypes.LIGHTBULB, name=LIGHT_BULB_NAME)
on_char = service.add_char(CharacteristicsTypes.ON)
on_char.value = 0
brightness = service.add_char(CharacteristicsTypes.BRIGHTNESS)
brightness.value = 0
return service
def create_lightbulb_service_with_hs(accessory: Accessory) -> Service:
"""Define a lightbulb service with hue + saturation."""
service = create_lightbulb_service(accessory)
hue = service.add_char(CharacteristicsTypes.HUE)
hue.value = 0
saturation = service.add_char(CharacteristicsTypes.SATURATION)
saturation.value = 0
return service
def create_lightbulb_service_with_color_temp(accessory: Accessory) -> Service:
"""Define a lightbulb service with color temp."""
service = create_lightbulb_service(accessory)
color_temp = service.add_char(CharacteristicsTypes.COLOR_TEMPERATURE)
color_temp.value = 0
return service
async def test_switch_change_light_state(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we can turn a HomeKit light on and off again."""
helper = await setup_test_component(
hass, get_next_aid(), create_lightbulb_service_with_hs
)
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": "light.testdevice", "brightness": 255, "hs_color": [4, 5]},
blocking=True,
)
helper.async_assert_service_values(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
CharacteristicsTypes.HUE: 4,
CharacteristicsTypes.SATURATION: 5,
},
)
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(
"light", "turn_off", {"entity_id": "light.testdevice"}, blocking=True
)
helper.async_assert_service_values(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: False,
},
)
async def test_switch_change_light_state_color_temp(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we can turn change color_temp."""
helper = await setup_test_component(
hass, get_next_aid(), create_lightbulb_service_with_color_temp
)
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": "light.testdevice", "brightness": 255, "color_temp": 400},
blocking=True,
)
helper.async_assert_service_values(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
CharacteristicsTypes.COLOR_TEMPERATURE: 400,
},
)
async def test_switch_read_light_state_dimmer(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we can read the state of a HomeKit light accessory."""
helper = await setup_test_component(hass, get_next_aid(), create_lightbulb_service)
# Initial state is that the light is off
state = await helper.poll_and_get_state()
assert state.state == "off"
assert state.attributes[ATTR_COLOR_MODE] is None
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS]
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
# Simulate that someone switched on the device in the real world not via HA
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
},
)
assert state.state == "on"
assert state.attributes["brightness"] == 255
assert state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS]
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
# Simulate that device switched off in the real world not via HA
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: False,
},
)
assert state.state == "off"
async def test_switch_push_light_state_dimmer(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we can read the state of a HomeKit light accessory."""
helper = await setup_test_component(hass, get_next_aid(), create_lightbulb_service)
# Initial state is that the light is off
state = hass.states.get(LIGHT_BULB_ENTITY_ID)
assert state.state == "off"
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
},
)
assert state.state == "on"
assert state.attributes["brightness"] == 255
# Simulate that device switched off in the real world not via HA
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: False,
},
)
assert state.state == "off"
async def test_switch_read_light_state_hs(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we can read the state of a HomeKit light accessory."""
helper = await setup_test_component(
hass, get_next_aid(), create_lightbulb_service_with_hs
)
# Initial state is that the light is off
state = await helper.poll_and_get_state()
assert state.state == "off"
assert state.attributes[ATTR_COLOR_MODE] is None
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [
ColorMode.COLOR_TEMP,
ColorMode.HS,
]
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
# Simulate that someone switched on the device in the real world not via HA
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
CharacteristicsTypes.HUE: 4,
CharacteristicsTypes.SATURATION: 5,
},
)
assert state.state == "on"
assert state.attributes["brightness"] == 255
assert state.attributes["hs_color"] == (4, 5)
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
# Simulate that device switched off in the real world not via HA
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: False,
},
)
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, get_next_aid: Callable[[], int]
) -> None:
"""Test that we can read the state of a HomeKit light accessory."""
helper = await setup_test_component(
hass, get_next_aid(), create_lightbulb_service_with_hs
)
# Initial state is that the light is off
state = hass.states.get(LIGHT_BULB_ENTITY_ID)
assert state.state == "off"
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
CharacteristicsTypes.HUE: 4,
CharacteristicsTypes.SATURATION: 5,
},
)
assert state.state == "on"
assert state.attributes["brightness"] == 255
assert state.attributes["hs_color"] == (4, 5)
# Simulate that device switched off in the real world not via HA
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: False,
},
)
assert state.state == "off"
async def test_switch_read_light_state_color_temp(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we can read the color_temp of a light accessory."""
helper = await setup_test_component(
hass, get_next_aid(), create_lightbulb_service_with_color_temp
)
# Initial state is that the light is off
state = await helper.poll_and_get_state()
assert state.state == "off"
assert state.attributes[ATTR_COLOR_MODE] is None
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP]
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
# Simulate that someone switched on the device in the real world not via HA
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
CharacteristicsTypes.COLOR_TEMPERATURE: 400,
},
)
assert state.state == "on"
assert state.attributes["brightness"] == 255
assert state.attributes["color_temp"] == 400
assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP
assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP]
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
async def test_switch_push_light_state_color_temp(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we can read the state of a HomeKit light accessory."""
helper = await setup_test_component(
hass, get_next_aid(), create_lightbulb_service_with_color_temp
)
# Initial state is that the light is off
state = hass.states.get(LIGHT_BULB_ENTITY_ID)
assert state.state == "off"
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
CharacteristicsTypes.COLOR_TEMPERATURE: 400,
},
)
assert state.state == "on"
assert state.attributes["brightness"] == 255
assert state.attributes["color_temp"] == 400
async def test_light_becomes_unavailable_but_recovers(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test transition to and from unavailable state."""
helper = await setup_test_component(
hass, get_next_aid(), create_lightbulb_service_with_color_temp
)
# Initial state is that the light is off
state = await helper.poll_and_get_state()
assert state.state == "off"
# Test device goes offline
helper.pairing.available = False
with mock.patch.object(
FakeController,
"async_reachable",
return_value=False,
):
state = await helper.poll_and_get_state()
assert state.state == "unavailable"
# Simulate that someone switched on the device in the real world not via HA
helper.pairing.available = True
state = await helper.async_update(
ServicesTypes.LIGHTBULB,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
CharacteristicsTypes.COLOR_TEMPERATURE: 400,
},
)
assert state.state == "on"
assert state.attributes["brightness"] == 255
assert state.attributes["color_temp"] == 400
async def test_light_unloaded_removed(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test entity and HKDevice are correctly unloaded and removed."""
helper = await setup_test_component(
hass, get_next_aid(), create_lightbulb_service_with_color_temp
)
# Initial state is that the light is off
state = await helper.poll_and_get_state()
assert state.state == "off"
unload_result = await hass.config_entries.async_unload(helper.config_entry.entry_id)
assert unload_result is True
# Make sure entity is set to unavailable state
assert hass.states.get(helper.entity_id).state == STATE_UNAVAILABLE
# Make sure HKDevice is no longer set to poll this accessory
conn = hass.data[KNOWN_DEVICES]["00:00:00:00:00:00"]
assert not conn.pollable_characteristics
await hass.config_entries.async_remove(helper.config_entry.entry_id)
await hass.async_block_till_done()
# Make sure entity is removed
assert hass.states.get(helper.entity_id) is None
async def test_migrate_unique_id(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
get_next_aid: Callable[[], int],
) -> None:
"""Test a we can migrate a light unique id."""
aid = get_next_aid()
light_entry = entity_registry.async_get_or_create(
"light",
"homekit_controller",
f"homekit-00:00:00:00:00:00-{aid}-8",
)
await setup_test_component(hass, aid, create_lightbulb_service_with_color_temp)
assert (
entity_registry.async_get(light_entry.entity_id).unique_id
== f"00:00:00:00:00:00_{aid}_8"
)
async def test_only_migrate_once(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
get_next_aid: Callable[[], int],
) -> None:
"""Test a we handle migration happening after an upgrade and than a downgrade and then an upgrade."""
aid = get_next_aid()
old_light_entry = entity_registry.async_get_or_create(
"light",
"homekit_controller",
f"homekit-00:00:00:00:00:00-{aid}-8",
)
new_light_entry = entity_registry.async_get_or_create(
"light",
"homekit_controller",
f"00:00:00:00:00:00_{aid}_8",
)
await setup_test_component(hass, aid, create_lightbulb_service_with_color_temp)
assert (
entity_registry.async_get(old_light_entry.entity_id).unique_id
== f"homekit-00:00:00:00:00:00-{aid}-8"
)
assert (
entity_registry.async_get(new_light_entry.entity_id).unique_id
== f"00:00:00:00:00:00_{aid}_8"
)