mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Cache emulated hue states attributes between get and put calls to avoid unexpected alexa errors (#38451)
* Wait for the state of the entity to actually change before resolving PUT request Additionally, we cache the entity's properties for up to two seconds for the successive GET state request When Alexa issues a command to a Hue hub; it immediately queries the hub for the entity's state to confirm if the command was successful. It expects the state to be effective immediately after the PUT request has been completed. There may be a delay for the new state to actually be active, this is particularly obvious when using group lights. This leads Alexa to report that the light had an error. So we wait for the state of the entity to actually change before responding to the PUT request. Due to rounding issue when converting the HA range (0..255) to Hue range (1..254) we now cache the state sets by Alexa and return those cached values for up to two seconds so that Alexa gets the same value as it originally set. Fixes #38446 * Add new tests verifying emulated_hue behaviour. * Increase code test coverage. The remaining uncovered lines can't be tested as they mostly check that the hass framework or the http server properly work. This commit doesn't attempt to fix exposed issues as it would be out of scope ; it merely create the tests to exercise the whole code. * Update homeassistant/components/emulated_hue/hue_api.py * Add test for state change wait timeout * Preserve the cache long enough for groups to change * Update tests/components/emulated_hue/test_hue_api.py Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
62c664fbbd
commit
988cbf12ce
@ -2,6 +2,7 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
import time
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components import (
|
||||
@ -66,10 +67,16 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.util.network import is_local
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# How long to wait for a state change to happen
|
||||
STATE_CHANGE_WAIT_TIMEOUT = 5.0
|
||||
# How long an entry state's cache will be valid for in seconds.
|
||||
STATE_CACHED_TIMEOUT = 2.0
|
||||
|
||||
STATE_BRIGHTNESS = "bri"
|
||||
STATE_COLORMODE = "colormode"
|
||||
STATE_HUE = "hue"
|
||||
@ -515,13 +522,6 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
if entity.domain in config.off_maps_to_on_domains:
|
||||
service = SERVICE_TURN_ON
|
||||
|
||||
# Caching is required because things like scripts and scenes won't
|
||||
# report as "off" to Alexa if an "off" command is received, because
|
||||
# they'll map to "on". Thus, instead of reporting its actual
|
||||
# status, we report what Alexa will want to see, which is the same
|
||||
# as the actual requested command.
|
||||
config.cached_states[entity_id] = parsed
|
||||
|
||||
# Separate call to turn on needed
|
||||
if turn_on_needed:
|
||||
hass.async_create_task(
|
||||
@ -534,10 +534,18 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
)
|
||||
|
||||
if service is not None:
|
||||
state_will_change = parsed[STATE_ON] != (entity.state != STATE_OFF)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(domain, service, data, blocking=True)
|
||||
)
|
||||
|
||||
if state_will_change:
|
||||
# Wait for the state to change.
|
||||
await wait_for_state_change_or_timeout(
|
||||
hass, entity_id, STATE_CACHED_TIMEOUT
|
||||
)
|
||||
|
||||
# Create success responses for all received keys
|
||||
json_response = [
|
||||
create_hue_success_response(
|
||||
@ -556,16 +564,40 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
create_hue_success_response(entity_number, val, parsed[key])
|
||||
)
|
||||
|
||||
# Echo fetches the state immediately after the PUT method returns.
|
||||
# Waiting for a short time allows the changes to propagate.
|
||||
await asyncio.sleep(0.25)
|
||||
if entity.domain in config.off_maps_to_on_domains:
|
||||
# Caching is required because things like scripts and scenes won't
|
||||
# report as "off" to Alexa if an "off" command is received, because
|
||||
# they'll map to "on". Thus, instead of reporting its actual
|
||||
# status, we report what Alexa will want to see, which is the same
|
||||
# as the actual requested command.
|
||||
config.cached_states[entity_id] = [parsed, None]
|
||||
else:
|
||||
config.cached_states[entity_id] = [parsed, time.time()]
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
|
||||
def get_entity_state(config, entity):
|
||||
"""Retrieve and convert state and brightness values for an entity."""
|
||||
cached_state = config.cached_states.get(entity.entity_id, None)
|
||||
cached_state_entry = config.cached_states.get(entity.entity_id, None)
|
||||
cached_state = None
|
||||
|
||||
# Check if we have a cached entry, and if so if it hasn't expired.
|
||||
if cached_state_entry is not None:
|
||||
entry_state, entry_time = cached_state_entry
|
||||
if entry_time is None:
|
||||
# Handle the case where the entity is listed in config.off_maps_to_on_domains.
|
||||
cached_state = entry_state
|
||||
elif time.time() - entry_time < STATE_CACHED_TIMEOUT and entry_state[
|
||||
STATE_ON
|
||||
] == (entity.state != STATE_OFF):
|
||||
# We only want to use the cache if the actual state of the entity
|
||||
# is in sync so that it can be detected as an error by Alexa.
|
||||
cached_state = entry_state
|
||||
else:
|
||||
# Remove the now stale cached entry.
|
||||
config.cached_states.pop(entity.entity_id)
|
||||
|
||||
data = {
|
||||
STATE_ON: False,
|
||||
STATE_BRIGHTNESS: None,
|
||||
@ -791,3 +823,21 @@ def hue_brightness_to_hass(value):
|
||||
def hass_to_hue_brightness(value):
|
||||
"""Convert hass brightness 0..255 to hue 1..254 scale."""
|
||||
return max(1, round((value / 255) * HUE_API_STATE_BRI_MAX))
|
||||
|
||||
|
||||
async def wait_for_state_change_or_timeout(hass, entity_id, timeout):
|
||||
"""Wait for an entity to change state."""
|
||||
ev = asyncio.Event()
|
||||
|
||||
@core.callback
|
||||
def _async_event_changed(_):
|
||||
ev.set()
|
||||
|
||||
unsub = async_track_state_change_event(hass, [entity_id], _async_event_changed)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(ev.wait(), timeout=STATE_CHANGE_WAIT_TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
finally:
|
||||
unsub()
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""The tests for the emulated Hue component."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from ipaddress import ip_address
|
||||
import json
|
||||
@ -18,9 +19,10 @@ from homeassistant.components import (
|
||||
media_player,
|
||||
script,
|
||||
)
|
||||
from homeassistant.components.emulated_hue import Config
|
||||
from homeassistant.components.emulated_hue import Config, hue_api
|
||||
from homeassistant.components.emulated_hue.hue_api import (
|
||||
HUE_API_STATE_BRI,
|
||||
HUE_API_STATE_CT,
|
||||
HUE_API_STATE_HUE,
|
||||
HUE_API_STATE_ON,
|
||||
HUE_API_STATE_SAT,
|
||||
@ -43,14 +45,11 @@ from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import (
|
||||
async_fire_time_changed,
|
||||
async_mock_service,
|
||||
get_test_instance_port,
|
||||
)
|
||||
from tests.common import async_fire_time_changed, get_test_instance_port
|
||||
|
||||
HTTP_SERVER_PORT = get_test_instance_port()
|
||||
BRIDGE_SERVER_PORT = get_test_instance_port()
|
||||
@ -77,6 +76,8 @@ ENTITY_IDS_BY_NUMBER = {
|
||||
"16": "humidifier.humidifier",
|
||||
"17": "humidifier.dehumidifier",
|
||||
"18": "humidifier.hygrostat",
|
||||
"19": "scene.light_on",
|
||||
"20": "scene.light_off",
|
||||
}
|
||||
|
||||
ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()}
|
||||
@ -164,6 +165,28 @@ def hass_hue(loop, hass):
|
||||
)
|
||||
)
|
||||
|
||||
# setup a dummy scene
|
||||
loop.run_until_complete(
|
||||
setup.async_setup_component(
|
||||
hass,
|
||||
"scene",
|
||||
{
|
||||
"scene": [
|
||||
{
|
||||
"id": "light_on",
|
||||
"name": "Light on",
|
||||
"entities": {"light.kitchen_lights": {"state": "on"}},
|
||||
},
|
||||
{
|
||||
"id": "light_off",
|
||||
"name": "Light off",
|
||||
"entities": {"light.kitchen_lights": {"state": "off"}},
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# create a lamp without brightness support
|
||||
hass.states.async_set("light.no_brightness", "on", {})
|
||||
|
||||
@ -197,6 +220,9 @@ def hue_client(loop, hass_hue, aiohttp_client):
|
||||
"humidifier.dehumidifier": {emulated_hue.CONF_ENTITY_HIDDEN: False},
|
||||
# No expose setting (use default of not exposed)
|
||||
"climate.nosetting": {},
|
||||
# Expose scenes
|
||||
"scene.light_on": {emulated_hue.CONF_ENTITY_HIDDEN: False},
|
||||
"scene.light_off": {emulated_hue.CONF_ENTITY_HIDDEN: False},
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -243,6 +269,8 @@ async def test_discover_lights(hue_client):
|
||||
assert "00:78:eb:f8:d5:0c:14:85-e7" in devices # humidifier.humidifier
|
||||
assert "00:67:19:bd:ea:e4:2d:ef-22" in devices # humidifier.dehumidifier
|
||||
assert "00:61:bf:ab:08:b1:a6:18-43" not in devices # humidifier.hygrostat
|
||||
assert "00:62:5c:3e:df:58:40:01-43" in devices # scene.light_on
|
||||
assert "00:1c:72:08:ed:09:e7:89-77" in devices # scene.light_off
|
||||
|
||||
|
||||
async def test_light_without_brightness_supported(hass_hue, hue_client):
|
||||
@ -258,9 +286,18 @@ async def test_light_without_brightness_supported(hass_hue, hue_client):
|
||||
async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client):
|
||||
"""Test that light without brightness can be turned off."""
|
||||
hass_hue.states.async_set("light.no_brightness", "on", {})
|
||||
turn_off_calls = []
|
||||
|
||||
# Check if light can be turned off
|
||||
turn_off_calls = async_mock_service(hass_hue, light.DOMAIN, SERVICE_TURN_OFF)
|
||||
@callback
|
||||
def mock_service_call(call):
|
||||
"""Mock service call."""
|
||||
turn_off_calls.append(call)
|
||||
hass_hue.states.async_set("light.no_brightness", "off", {})
|
||||
|
||||
hass_hue.services.async_register(
|
||||
light.DOMAIN, SERVICE_TURN_OFF, mock_service_call, schema=None
|
||||
)
|
||||
|
||||
no_brightness_result = await perform_put_light_state(
|
||||
hass_hue, hue_client, "light.no_brightness", False
|
||||
@ -286,7 +323,17 @@ async def test_light_without_brightness_can_be_turned_on(hass_hue, hue_client):
|
||||
hass_hue.states.async_set("light.no_brightness", "off", {})
|
||||
|
||||
# Check if light can be turned on
|
||||
turn_on_calls = async_mock_service(hass_hue, light.DOMAIN, SERVICE_TURN_ON)
|
||||
turn_on_calls = []
|
||||
|
||||
@callback
|
||||
def mock_service_call(call):
|
||||
"""Mock service call."""
|
||||
turn_on_calls.append(call)
|
||||
hass_hue.states.async_set("light.no_brightness", "on", {})
|
||||
|
||||
hass_hue.services.async_register(
|
||||
light.DOMAIN, SERVICE_TURN_ON, mock_service_call, schema=None
|
||||
)
|
||||
|
||||
no_brightness_result = await perform_put_light_state(
|
||||
hass_hue,
|
||||
@ -423,7 +470,7 @@ async def test_discover_config(hue_client):
|
||||
|
||||
async def test_get_light_state(hass_hue, hue_client):
|
||||
"""Test the getting of light state."""
|
||||
# Turn office light on and set to 127 brightness, and set light color
|
||||
# Turn ceiling lights on and set to 127 brightness, and set light color
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_ON,
|
||||
@ -591,6 +638,23 @@ async def test_put_light_state(hass, hass_hue, hue_client):
|
||||
)
|
||||
assert kitchen_result.status == HTTP_UNAUTHORIZED
|
||||
|
||||
# Turn the ceiling lights on first and color temp.
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_ON,
|
||||
{const.ATTR_ENTITY_ID: "light.ceiling_lights", light.ATTR_COLOR_TEMP: 20},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await perform_put_light_state(
|
||||
hass_hue, hue_client, "light.ceiling_lights", True, color_temp=50
|
||||
)
|
||||
|
||||
assert (
|
||||
hass_hue.states.get("light.ceiling_lights").attributes[light.ATTR_COLOR_TEMP]
|
||||
== 50
|
||||
)
|
||||
|
||||
|
||||
async def test_put_light_state_script(hass, hass_hue, hue_client):
|
||||
"""Test the setting of script variables."""
|
||||
@ -832,6 +896,71 @@ async def test_put_light_state_fan(hass_hue, hue_client):
|
||||
assert living_room_fan.state == "on"
|
||||
assert living_room_fan.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
|
||||
|
||||
# Check setting the brightness of a fan to 0, 33%, 66% and 100% will respectively turn it off, low, medium or high
|
||||
# We also check non-cached GET value to exercise the code.
|
||||
await perform_put_light_state(
|
||||
hass_hue, hue_client, "fan.living_room_fan", True, brightness=0
|
||||
)
|
||||
assert (
|
||||
hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED]
|
||||
== fan.SPEED_OFF
|
||||
)
|
||||
await perform_put_light_state(
|
||||
hass_hue,
|
||||
hue_client,
|
||||
"fan.living_room_fan",
|
||||
True,
|
||||
brightness=round(33 * 254 / 100),
|
||||
)
|
||||
assert (
|
||||
hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED]
|
||||
== fan.SPEED_LOW
|
||||
)
|
||||
with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001):
|
||||
await asyncio.sleep(0.000001)
|
||||
fan_json = await perform_get_light_state(
|
||||
hue_client, "fan.living_room_fan", HTTP_OK
|
||||
)
|
||||
assert round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 33
|
||||
|
||||
await perform_put_light_state(
|
||||
hass_hue,
|
||||
hue_client,
|
||||
"fan.living_room_fan",
|
||||
True,
|
||||
brightness=round(66 * 254 / 100),
|
||||
)
|
||||
assert (
|
||||
hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED]
|
||||
== fan.SPEED_MEDIUM
|
||||
)
|
||||
with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001):
|
||||
await asyncio.sleep(0.000001)
|
||||
fan_json = await perform_get_light_state(
|
||||
hue_client, "fan.living_room_fan", HTTP_OK
|
||||
)
|
||||
assert (
|
||||
round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 67
|
||||
) # small rounding error in inverse operation
|
||||
|
||||
await perform_put_light_state(
|
||||
hass_hue,
|
||||
hue_client,
|
||||
"fan.living_room_fan",
|
||||
True,
|
||||
brightness=round(100 * 254 / 100),
|
||||
)
|
||||
assert (
|
||||
hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED]
|
||||
== fan.SPEED_HIGH
|
||||
)
|
||||
with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001):
|
||||
await asyncio.sleep(0.000001)
|
||||
fan_json = await perform_get_light_state(
|
||||
hue_client, "fan.living_room_fan", HTTP_OK
|
||||
)
|
||||
assert round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 100
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client):
|
||||
@ -952,9 +1081,8 @@ async def perform_put_test_on_ceiling_lights(
|
||||
assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 56
|
||||
|
||||
|
||||
async def perform_get_light_state(client, entity_id, expected_status):
|
||||
async def perform_get_light_state_by_number(client, entity_number, expected_status):
|
||||
"""Test the getting of a light state."""
|
||||
entity_number = ENTITY_NUMBERS_BY_ID[entity_id]
|
||||
result = await client.get(f"/api/username/lights/{entity_number}")
|
||||
|
||||
assert result.status == expected_status
|
||||
@ -967,6 +1095,14 @@ async def perform_get_light_state(client, entity_id, expected_status):
|
||||
return None
|
||||
|
||||
|
||||
async def perform_get_light_state(client, entity_id, expected_status):
|
||||
"""Test the getting of a light state."""
|
||||
entity_number = ENTITY_NUMBERS_BY_ID[entity_id]
|
||||
return await perform_get_light_state_by_number(
|
||||
client, entity_number, expected_status
|
||||
)
|
||||
|
||||
|
||||
async def perform_put_light_state(
|
||||
hass_hue,
|
||||
client,
|
||||
@ -976,11 +1112,16 @@ async def perform_put_light_state(
|
||||
content_type="application/json",
|
||||
hue=None,
|
||||
saturation=None,
|
||||
color_temp=None,
|
||||
with_state=True,
|
||||
):
|
||||
"""Test the setting of a light state."""
|
||||
req_headers = {"Content-Type": content_type}
|
||||
|
||||
data = {HUE_API_STATE_ON: is_on}
|
||||
data = {}
|
||||
|
||||
if with_state:
|
||||
data[HUE_API_STATE_ON] = is_on
|
||||
|
||||
if brightness is not None:
|
||||
data[HUE_API_STATE_BRI] = brightness
|
||||
@ -988,6 +1129,8 @@ async def perform_put_light_state(
|
||||
data[HUE_API_STATE_HUE] = hue
|
||||
if saturation is not None:
|
||||
data[HUE_API_STATE_SAT] = saturation
|
||||
if color_temp is not None:
|
||||
data[HUE_API_STATE_CT] = color_temp
|
||||
|
||||
entity_number = ENTITY_NUMBERS_BY_ID[entity_id]
|
||||
result = await client.put(
|
||||
@ -1042,3 +1185,291 @@ async def test_unauthorized_user_blocked(hue_client):
|
||||
|
||||
result_json = await result.json()
|
||||
assert result_json[0]["error"]["description"] == "unauthorized user"
|
||||
|
||||
|
||||
async def test_put_then_get_cached_properly(hass, hass_hue, hue_client):
|
||||
"""Test the setting of light states and an immediate readback reads the same values."""
|
||||
|
||||
# Turn the bedroom light on first
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_ON,
|
||||
{const.ATTR_ENTITY_ID: "light.ceiling_lights", light.ATTR_BRIGHTNESS: 153},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
ceiling_lights = hass_hue.states.get("light.ceiling_lights")
|
||||
assert ceiling_lights.state == STATE_ON
|
||||
assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 153
|
||||
|
||||
# update light state through api
|
||||
await perform_put_light_state(
|
||||
hass_hue,
|
||||
hue_client,
|
||||
"light.ceiling_lights",
|
||||
True,
|
||||
hue=4369,
|
||||
saturation=127,
|
||||
brightness=254,
|
||||
)
|
||||
|
||||
# Check that a Hue brightness level of 254 becomes 255 in HA realm.
|
||||
assert (
|
||||
hass.states.get("light.ceiling_lights").attributes[light.ATTR_BRIGHTNESS] == 255
|
||||
)
|
||||
|
||||
# Make sure that the GET response is the same as the PUT response within 2 seconds if the service call is successful and the state doesn't change.
|
||||
# We simulate a long latence for the actual setting of the entity by forcibly sitting different values directly.
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_ON,
|
||||
{const.ATTR_ENTITY_ID: "light.ceiling_lights", light.ATTR_BRIGHTNESS: 153},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# go through api to get the state back, the value returned should match those set in the last PUT request.
|
||||
ceiling_json = await perform_get_light_state(
|
||||
hue_client, "light.ceiling_lights", HTTP_OK
|
||||
)
|
||||
|
||||
assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369
|
||||
assert ceiling_json["state"][HUE_API_STATE_SAT] == 127
|
||||
assert ceiling_json["state"][HUE_API_STATE_BRI] == 254
|
||||
|
||||
# Make sure that the GET response does not use the cache if PUT response within 2 seconds if the service call is Unsuccessful and the state does not change.
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_OFF,
|
||||
{const.ATTR_ENTITY_ID: "light.ceiling_lights"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# go through api to get the state back
|
||||
ceiling_json = await perform_get_light_state(
|
||||
hue_client, "light.ceiling_lights", HTTP_OK
|
||||
)
|
||||
|
||||
# Now it should be the real value as the state of the entity has changed to OFF.
|
||||
assert ceiling_json["state"][HUE_API_STATE_HUE] == 0
|
||||
assert ceiling_json["state"][HUE_API_STATE_SAT] == 0
|
||||
assert ceiling_json["state"][HUE_API_STATE_BRI] == 1
|
||||
|
||||
# Ensure we read the actual value after exceeding the timeout time.
|
||||
|
||||
# Turn the bedroom light back on first
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_ON,
|
||||
{const.ATTR_ENTITY_ID: "light.ceiling_lights"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# update light state through api
|
||||
await perform_put_light_state(
|
||||
hass_hue,
|
||||
hue_client,
|
||||
"light.ceiling_lights",
|
||||
True,
|
||||
hue=4369,
|
||||
saturation=127,
|
||||
brightness=254,
|
||||
)
|
||||
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_ON,
|
||||
{
|
||||
const.ATTR_ENTITY_ID: "light.ceiling_lights",
|
||||
light.ATTR_BRIGHTNESS: 127,
|
||||
light.ATTR_RGB_COLOR: (1, 2, 7),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# go through api to get the state back, the value returned should match those set in the last PUT request.
|
||||
ceiling_json = await perform_get_light_state(
|
||||
hue_client, "light.ceiling_lights", HTTP_OK
|
||||
)
|
||||
|
||||
# With no wait, we must be reading what we set via the PUT call.
|
||||
assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369
|
||||
assert ceiling_json["state"][HUE_API_STATE_SAT] == 127
|
||||
assert ceiling_json["state"][HUE_API_STATE_BRI] == 254
|
||||
|
||||
with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001):
|
||||
await asyncio.sleep(0.000001)
|
||||
|
||||
# go through api to get the state back, the value returned should now match the actual values.
|
||||
ceiling_json = await perform_get_light_state(
|
||||
hue_client, "light.ceiling_lights", HTTP_OK
|
||||
)
|
||||
|
||||
# Once we're after the cached duration, we should see the real value.
|
||||
assert ceiling_json["state"][HUE_API_STATE_HUE] == 41869
|
||||
assert ceiling_json["state"][HUE_API_STATE_SAT] == 217
|
||||
assert ceiling_json["state"][HUE_API_STATE_BRI] == 127
|
||||
|
||||
|
||||
async def test_put_than_get_when_service_call_fails(hass, hass_hue, hue_client):
|
||||
"""Test putting and getting the light state when the service call fails."""
|
||||
|
||||
# Turn the bedroom light off first
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_OFF,
|
||||
{const.ATTR_ENTITY_ID: "light.ceiling_lights"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
turn_on_calls = []
|
||||
|
||||
# Now break the turn on service
|
||||
@callback
|
||||
def mock_service_call(call):
|
||||
"""Mock service call."""
|
||||
turn_on_calls.append(call)
|
||||
|
||||
hass_hue.services.async_register(
|
||||
light.DOMAIN, SERVICE_TURN_ON, mock_service_call, schema=None
|
||||
)
|
||||
|
||||
ceiling_lights = hass_hue.states.get("light.ceiling_lights")
|
||||
assert ceiling_lights.state == STATE_OFF
|
||||
|
||||
with patch.object(hue_api, "STATE_CHANGE_WAIT_TIMEOUT", 0.000001):
|
||||
# update light state through api
|
||||
await perform_put_light_state(
|
||||
hass_hue,
|
||||
hue_client,
|
||||
"light.ceiling_lights",
|
||||
True,
|
||||
hue=4369,
|
||||
saturation=127,
|
||||
brightness=254,
|
||||
)
|
||||
|
||||
# Ensure we did not actually turn on
|
||||
assert hass.states.get("light.ceiling_lights").state == STATE_OFF
|
||||
|
||||
# go through api to get the state back, the value returned should NOT match those set in the last PUT request
|
||||
# as the waiting to check the state change timed out
|
||||
ceiling_json = await perform_get_light_state(
|
||||
hue_client, "light.ceiling_lights", HTTP_OK
|
||||
)
|
||||
|
||||
assert ceiling_json["state"][HUE_API_STATE_ON] is False
|
||||
|
||||
|
||||
async def test_get_invalid_entity(hass, hass_hue, hue_client):
|
||||
"""Test the setting of light states and an immediate readback reads the same values."""
|
||||
|
||||
# Check that we get an error with an invalid entity number.
|
||||
await perform_get_light_state_by_number(hue_client, 999, HTTP_NOT_FOUND)
|
||||
|
||||
|
||||
async def test_put_light_state_scene(hass, hass_hue, hue_client):
|
||||
"""Test the setting of scene variables."""
|
||||
# Turn the kitchen lights off first
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_OFF,
|
||||
{const.ATTR_ENTITY_ID: "light.kitchen_lights"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
scene_result = await perform_put_light_state(
|
||||
hass_hue, hue_client, "scene.light_on", True
|
||||
)
|
||||
|
||||
scene_result_json = await scene_result.json()
|
||||
assert scene_result.status == HTTP_OK
|
||||
assert len(scene_result_json) == 1
|
||||
|
||||
assert hass_hue.states.get("light.kitchen_lights").state == STATE_ON
|
||||
|
||||
# Set the brightness on the entity; changing a scene brightness via the hue API will do nothing.
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_ON,
|
||||
{const.ATTR_ENTITY_ID: "light.kitchen_lights", light.ATTR_BRIGHTNESS: 127},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await perform_put_light_state(
|
||||
hass_hue, hue_client, "scene.light_on", True, brightness=254
|
||||
)
|
||||
|
||||
assert hass_hue.states.get("light.kitchen_lights").state == STATE_ON
|
||||
assert (
|
||||
hass_hue.states.get("light.kitchen_lights").attributes[light.ATTR_BRIGHTNESS]
|
||||
== 127
|
||||
)
|
||||
|
||||
await perform_put_light_state(hass_hue, hue_client, "scene.light_off", True)
|
||||
assert hass_hue.states.get("light.kitchen_lights").state == STATE_OFF
|
||||
|
||||
|
||||
async def test_only_change_contrast(hass, hass_hue, hue_client):
|
||||
"""Test when only changing the contrast of a light state."""
|
||||
|
||||
# Turn the kitchen lights off first
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_OFF,
|
||||
{const.ATTR_ENTITY_ID: "light.ceiling_lights"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await perform_put_light_state(
|
||||
hass_hue,
|
||||
hue_client,
|
||||
"light.ceiling_lights",
|
||||
True,
|
||||
brightness=254,
|
||||
with_state=False,
|
||||
)
|
||||
|
||||
# Check that only setting the contrast will also turn on the light.
|
||||
# TODO: It should be noted that a real Hue hub will not allow to change the brightness if the underlying entity is off.
|
||||
# giving the error: [{"error":{"type":201,"address":"/lights/20/state/bri","description":"parameter, bri, is not modifiable. Device is set to off."}}]
|
||||
# emulated_hue however will always turn on the light.
|
||||
ceiling_lights = hass_hue.states.get("light.ceiling_lights")
|
||||
assert ceiling_lights.state == STATE_ON
|
||||
assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 255
|
||||
|
||||
|
||||
async def test_only_change_hue_or_saturation(hass, hass_hue, hue_client):
|
||||
"""Test setting either the hue or the saturation but not both."""
|
||||
|
||||
# TODO: The handling of this appears wrong, as setting only one will set the other to 0.
|
||||
# The return values also appear wrong.
|
||||
|
||||
# Turn the ceiling lights on first and set hue and saturation.
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_ON,
|
||||
{const.ATTR_ENTITY_ID: "light.ceiling_lights", light.ATTR_HS_COLOR: (10, 10)},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await perform_put_light_state(
|
||||
hass_hue, hue_client, "light.ceiling_lights", True, hue=4369
|
||||
)
|
||||
|
||||
assert hass_hue.states.get("light.ceiling_lights").attributes[
|
||||
light.ATTR_HS_COLOR
|
||||
] == (24, 0)
|
||||
|
||||
await hass_hue.services.async_call(
|
||||
light.DOMAIN,
|
||||
const.SERVICE_TURN_ON,
|
||||
{const.ATTR_ENTITY_ID: "light.ceiling_lights", light.ATTR_HS_COLOR: (10, 10)},
|
||||
blocking=True,
|
||||
)
|
||||
await perform_put_light_state(
|
||||
hass_hue, hue_client, "light.ceiling_lights", True, saturation=10
|
||||
)
|
||||
|
||||
assert hass_hue.states.get("light.ceiling_lights").attributes[
|
||||
light.ATTR_HS_COLOR
|
||||
] == (0, 3)
|
||||
|
Loading…
x
Reference in New Issue
Block a user