mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 04:07:08 +00:00
Overhaul Emulated Hue (#28317)
* Emulated Hue Overhaul * Fix erroneous merge * Remove unused code * Modernize string format
This commit is contained in:
parent
19241f421b
commit
3f2b6bfaa4
@ -1,7 +1,6 @@
|
|||||||
"""Support for a Hue API to control Home Assistant."""
|
"""Support for a Hue API to control Home Assistant."""
|
||||||
import logging
|
import logging
|
||||||
|
import hashlib
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
from homeassistant import core
|
from homeassistant import core
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
@ -36,8 +35,10 @@ from homeassistant.components.http.const import KEY_REAL_IP
|
|||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_HS_COLOR,
|
ATTR_HS_COLOR,
|
||||||
|
ATTR_COLOR_TEMP,
|
||||||
SUPPORT_BRIGHTNESS,
|
SUPPORT_BRIGHTNESS,
|
||||||
SUPPORT_COLOR,
|
SUPPORT_COLOR,
|
||||||
|
SUPPORT_COLOR_TEMP,
|
||||||
)
|
)
|
||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
ATTR_MEDIA_VOLUME_LEVEL,
|
ATTR_MEDIA_VOLUME_LEVEL,
|
||||||
@ -48,6 +49,7 @@ from homeassistant.const import (
|
|||||||
ATTR_SUPPORTED_FEATURES,
|
ATTR_SUPPORTED_FEATURES,
|
||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
HTTP_BAD_REQUEST,
|
HTTP_BAD_REQUEST,
|
||||||
|
HTTP_UNAUTHORIZED,
|
||||||
HTTP_NOT_FOUND,
|
HTTP_NOT_FOUND,
|
||||||
SERVICE_CLOSE_COVER,
|
SERVICE_CLOSE_COVER,
|
||||||
SERVICE_OPEN_COVER,
|
SERVICE_OPEN_COVER,
|
||||||
@ -62,18 +64,30 @@ from homeassistant.util.network import is_local
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STATE_BRIGHTNESS = "bri"
|
||||||
|
STATE_COLORMODE = "colormode"
|
||||||
|
STATE_HUE = "hue"
|
||||||
|
STATE_SATURATION = "sat"
|
||||||
|
STATE_COLOR_TEMP = "ct"
|
||||||
|
|
||||||
|
# Hue API states, defined separately in case they change
|
||||||
HUE_API_STATE_ON = "on"
|
HUE_API_STATE_ON = "on"
|
||||||
HUE_API_STATE_BRI = "bri"
|
HUE_API_STATE_BRI = "bri"
|
||||||
|
HUE_API_STATE_COLORMODE = "colormode"
|
||||||
HUE_API_STATE_HUE = "hue"
|
HUE_API_STATE_HUE = "hue"
|
||||||
HUE_API_STATE_SAT = "sat"
|
HUE_API_STATE_SAT = "sat"
|
||||||
|
HUE_API_STATE_CT = "ct"
|
||||||
|
HUE_API_STATE_EFFECT = "effect"
|
||||||
|
|
||||||
HUE_API_STATE_HUE_MAX = 65535.0
|
# Hue API min/max values - https://developers.meethue.com/develop/hue-api/lights-api/
|
||||||
HUE_API_STATE_SAT_MAX = 254.0
|
HUE_API_STATE_BRI_MIN = 1 # Brightness
|
||||||
HUE_API_STATE_BRI_MAX = 255.0
|
HUE_API_STATE_BRI_MAX = 254
|
||||||
|
HUE_API_STATE_HUE_MIN = 0 # Hue
|
||||||
STATE_BRIGHTNESS = HUE_API_STATE_BRI
|
HUE_API_STATE_HUE_MAX = 65535
|
||||||
STATE_HUE = HUE_API_STATE_HUE
|
HUE_API_STATE_SAT_MIN = 0 # Saturation
|
||||||
STATE_SATURATION = HUE_API_STATE_SAT
|
HUE_API_STATE_SAT_MAX = 254
|
||||||
|
HUE_API_STATE_CT_MIN = 153 # Color temp
|
||||||
|
HUE_API_STATE_CT_MAX = 500
|
||||||
|
|
||||||
|
|
||||||
class HueUsernameView(HomeAssistantView):
|
class HueUsernameView(HomeAssistantView):
|
||||||
@ -86,6 +100,9 @@ class HueUsernameView(HomeAssistantView):
|
|||||||
|
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
"""Handle a POST request."""
|
"""Handle a POST request."""
|
||||||
|
if not is_local(request[KEY_REAL_IP]):
|
||||||
|
return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -94,14 +111,11 @@ class HueUsernameView(HomeAssistantView):
|
|||||||
if "devicetype" not in data:
|
if "devicetype" not in data:
|
||||||
return self.json_message("devicetype not specified", HTTP_BAD_REQUEST)
|
return self.json_message("devicetype not specified", HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
if not is_local(request[KEY_REAL_IP]):
|
|
||||||
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
|
||||||
|
|
||||||
return self.json([{"success": {"username": "12345678901234567890"}}])
|
return self.json([{"success": {"username": "12345678901234567890"}}])
|
||||||
|
|
||||||
|
|
||||||
class HueAllGroupsStateView(HomeAssistantView):
|
class HueAllGroupsStateView(HomeAssistantView):
|
||||||
"""Group handler."""
|
"""Handle requests for getting info about entity groups."""
|
||||||
|
|
||||||
url = "/api/{username}/groups"
|
url = "/api/{username}/groups"
|
||||||
name = "emulated_hue:all_groups:state"
|
name = "emulated_hue:all_groups:state"
|
||||||
@ -115,7 +129,7 @@ class HueAllGroupsStateView(HomeAssistantView):
|
|||||||
def get(self, request, username):
|
def get(self, request, username):
|
||||||
"""Process a request to make the Brilliant Lightpad work."""
|
"""Process a request to make the Brilliant Lightpad work."""
|
||||||
if not is_local(request[KEY_REAL_IP]):
|
if not is_local(request[KEY_REAL_IP]):
|
||||||
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
return self.json({})
|
return self.json({})
|
||||||
|
|
||||||
@ -135,7 +149,7 @@ class HueGroupView(HomeAssistantView):
|
|||||||
def put(self, request, username):
|
def put(self, request, username):
|
||||||
"""Process a request to make the Logitech Pop working."""
|
"""Process a request to make the Logitech Pop working."""
|
||||||
if not is_local(request[KEY_REAL_IP]):
|
if not is_local(request[KEY_REAL_IP]):
|
||||||
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
return self.json(
|
return self.json(
|
||||||
[
|
[
|
||||||
@ -151,7 +165,7 @@ class HueGroupView(HomeAssistantView):
|
|||||||
|
|
||||||
|
|
||||||
class HueAllLightsStateView(HomeAssistantView):
|
class HueAllLightsStateView(HomeAssistantView):
|
||||||
"""Handle requests for getting and setting info about entities."""
|
"""Handle requests for getting info about all entities."""
|
||||||
|
|
||||||
url = "/api/{username}/lights"
|
url = "/api/{username}/lights"
|
||||||
name = "emulated_hue:lights:state"
|
name = "emulated_hue:lights:state"
|
||||||
@ -165,23 +179,21 @@ class HueAllLightsStateView(HomeAssistantView):
|
|||||||
def get(self, request, username):
|
def get(self, request, username):
|
||||||
"""Process a request to get the list of available lights."""
|
"""Process a request to get the list of available lights."""
|
||||||
if not is_local(request[KEY_REAL_IP]):
|
if not is_local(request[KEY_REAL_IP]):
|
||||||
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
json_response = {}
|
json_response = {}
|
||||||
|
|
||||||
for entity in hass.states.async_all():
|
for entity in hass.states.async_all():
|
||||||
if self.config.is_entity_exposed(entity):
|
if self.config.is_entity_exposed(entity):
|
||||||
state = get_entity_state(self.config, entity)
|
|
||||||
|
|
||||||
number = self.config.entity_id_to_number(entity.entity_id)
|
number = self.config.entity_id_to_number(entity.entity_id)
|
||||||
json_response[number] = entity_to_json(self.config, entity, state)
|
json_response[number] = entity_to_json(self.config, entity)
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
|
|
||||||
class HueOneLightStateView(HomeAssistantView):
|
class HueOneLightStateView(HomeAssistantView):
|
||||||
"""Handle requests for getting and setting info about entities."""
|
"""Handle requests for getting info about a single entity."""
|
||||||
|
|
||||||
url = "/api/{username}/lights/{entity_id}"
|
url = "/api/{username}/lights/{entity_id}"
|
||||||
name = "emulated_hue:light:state"
|
name = "emulated_hue:light:state"
|
||||||
@ -195,7 +207,7 @@ class HueOneLightStateView(HomeAssistantView):
|
|||||||
def get(self, request, username, entity_id):
|
def get(self, request, username, entity_id):
|
||||||
"""Process a request to get the state of an individual light."""
|
"""Process a request to get the state of an individual light."""
|
||||||
if not is_local(request[KEY_REAL_IP]):
|
if not is_local(request[KEY_REAL_IP]):
|
||||||
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
hass_entity_id = self.config.number_to_entity_id(entity_id)
|
hass_entity_id = self.config.number_to_entity_id(entity_id)
|
||||||
@ -205,27 +217,25 @@ class HueOneLightStateView(HomeAssistantView):
|
|||||||
"Unknown entity number: %s not found in emulated_hue_ids.json",
|
"Unknown entity number: %s not found in emulated_hue_ids.json",
|
||||||
entity_id,
|
entity_id,
|
||||||
)
|
)
|
||||||
return web.Response(text="Entity not found", status=404)
|
return self.json_message("Entity not found", HTTP_NOT_FOUND)
|
||||||
|
|
||||||
entity = hass.states.get(hass_entity_id)
|
entity = hass.states.get(hass_entity_id)
|
||||||
|
|
||||||
if entity is None:
|
if entity is None:
|
||||||
_LOGGER.error("Entity not found: %s", hass_entity_id)
|
_LOGGER.error("Entity not found: %s", hass_entity_id)
|
||||||
return web.Response(text="Entity not found", status=404)
|
return self.json_message("Entity not found", HTTP_NOT_FOUND)
|
||||||
|
|
||||||
if not self.config.is_entity_exposed(entity):
|
if not self.config.is_entity_exposed(entity):
|
||||||
_LOGGER.error("Entity not exposed: %s", entity_id)
|
_LOGGER.error("Entity not exposed: %s", entity_id)
|
||||||
return web.Response(text="Entity not exposed", status=404)
|
return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
state = get_entity_state(self.config, entity)
|
json_response = entity_to_json(self.config, entity)
|
||||||
|
|
||||||
json_response = entity_to_json(self.config, entity, state)
|
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
|
|
||||||
class HueOneLightChangeView(HomeAssistantView):
|
class HueOneLightChangeView(HomeAssistantView):
|
||||||
"""Handle requests for getting and setting info about entities."""
|
"""Handle requests for setting info about entities."""
|
||||||
|
|
||||||
url = "/api/{username}/lights/{entity_number}/state"
|
url = "/api/{username}/lights/{entity_number}/state"
|
||||||
name = "emulated_hue:light:state"
|
name = "emulated_hue:light:state"
|
||||||
@ -238,7 +248,7 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
async def put(self, request, username, entity_number):
|
async def put(self, request, username, entity_number):
|
||||||
"""Process a request to set the state of an individual light."""
|
"""Process a request to set the state of an individual light."""
|
||||||
if not is_local(request[KEY_REAL_IP]):
|
if not is_local(request[KEY_REAL_IP]):
|
||||||
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
config = self.config
|
config = self.config
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
@ -256,7 +266,7 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
|
|
||||||
if not config.is_entity_exposed(entity):
|
if not config.is_entity_exposed(entity):
|
||||||
_LOGGER.error("Entity not exposed: %s", entity_id)
|
_LOGGER.error("Entity not exposed: %s", entity_id)
|
||||||
return web.Response(text="Entity not exposed", status=404)
|
return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request_json = await request.json()
|
request_json = await request.json()
|
||||||
@ -264,12 +274,60 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
_LOGGER.error("Received invalid json")
|
_LOGGER.error("Received invalid json")
|
||||||
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
|
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
# Parse the request into requested "on" status and brightness
|
# Get the entity's supported features
|
||||||
parsed = parse_hue_api_put_light_body(request_json, entity)
|
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
if parsed is None:
|
# Parse the request
|
||||||
_LOGGER.error("Unable to parse data: %s", request_json)
|
parsed = {
|
||||||
return web.Response(text="Bad request", status=400)
|
STATE_ON: False,
|
||||||
|
STATE_BRIGHTNESS: None,
|
||||||
|
STATE_HUE: None,
|
||||||
|
STATE_SATURATION: None,
|
||||||
|
STATE_COLOR_TEMP: None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if HUE_API_STATE_ON in request_json:
|
||||||
|
if not isinstance(request_json[HUE_API_STATE_ON], bool):
|
||||||
|
_LOGGER.error("Unable to parse data: %s", request_json)
|
||||||
|
return self.json_message("Bad request", HTTP_BAD_REQUEST)
|
||||||
|
parsed[STATE_ON] = request_json[HUE_API_STATE_ON]
|
||||||
|
else:
|
||||||
|
parsed[STATE_ON] = entity.state != STATE_OFF
|
||||||
|
|
||||||
|
for (key, attr) in (
|
||||||
|
(HUE_API_STATE_BRI, STATE_BRIGHTNESS),
|
||||||
|
(HUE_API_STATE_HUE, STATE_HUE),
|
||||||
|
(HUE_API_STATE_SAT, STATE_SATURATION),
|
||||||
|
(HUE_API_STATE_CT, STATE_COLOR_TEMP),
|
||||||
|
):
|
||||||
|
if key in request_json:
|
||||||
|
try:
|
||||||
|
parsed[attr] = int(request_json[key])
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error("Unable to parse data (2): %s", request_json)
|
||||||
|
return self.json_message("Bad request", HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
|
if HUE_API_STATE_BRI in request_json:
|
||||||
|
if entity.domain == light.DOMAIN:
|
||||||
|
parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0
|
||||||
|
if not entity_features & SUPPORT_BRIGHTNESS:
|
||||||
|
parsed[STATE_BRIGHTNESS] = None
|
||||||
|
|
||||||
|
elif entity.domain == scene.DOMAIN:
|
||||||
|
parsed[STATE_BRIGHTNESS] = None
|
||||||
|
parsed[STATE_ON] = True
|
||||||
|
|
||||||
|
elif entity.domain in [
|
||||||
|
script.DOMAIN,
|
||||||
|
media_player.DOMAIN,
|
||||||
|
fan.DOMAIN,
|
||||||
|
cover.DOMAIN,
|
||||||
|
climate.DOMAIN,
|
||||||
|
]:
|
||||||
|
# Convert 0-255 to 0-100
|
||||||
|
level = (parsed[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100
|
||||||
|
parsed[STATE_BRIGHTNESS] = round(level)
|
||||||
|
parsed[STATE_ON] = True
|
||||||
|
|
||||||
# Choose general HA domain
|
# Choose general HA domain
|
||||||
domain = core.DOMAIN
|
domain = core.DOMAIN
|
||||||
@ -283,29 +341,37 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
# Construct what we need to send to the service
|
# Construct what we need to send to the service
|
||||||
data = {ATTR_ENTITY_ID: entity_id}
|
data = {ATTR_ENTITY_ID: entity_id}
|
||||||
|
|
||||||
# Make sure the entity actually supports brightness
|
# If the requested entity is a light, set the brightness, hue,
|
||||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
# saturation and color temp
|
||||||
|
|
||||||
if entity.domain == light.DOMAIN:
|
if entity.domain == light.DOMAIN:
|
||||||
if parsed[STATE_ON]:
|
if parsed[STATE_ON]:
|
||||||
if entity_features & SUPPORT_BRIGHTNESS:
|
if entity_features & SUPPORT_BRIGHTNESS:
|
||||||
if parsed[STATE_BRIGHTNESS] is not None:
|
if parsed[STATE_BRIGHTNESS] is not None:
|
||||||
data[ATTR_BRIGHTNESS] = parsed[STATE_BRIGHTNESS]
|
data[ATTR_BRIGHTNESS] = parsed[STATE_BRIGHTNESS]
|
||||||
|
|
||||||
if entity_features & SUPPORT_COLOR:
|
if entity_features & SUPPORT_COLOR:
|
||||||
if parsed[STATE_HUE] is not None:
|
if any((parsed[STATE_HUE], parsed[STATE_SATURATION])):
|
||||||
if parsed[STATE_SATURATION]:
|
if parsed[STATE_HUE] is not None:
|
||||||
|
hue = parsed[STATE_HUE]
|
||||||
|
else:
|
||||||
|
hue = 0
|
||||||
|
|
||||||
|
if parsed[STATE_SATURATION] is not None:
|
||||||
sat = parsed[STATE_SATURATION]
|
sat = parsed[STATE_SATURATION]
|
||||||
else:
|
else:
|
||||||
sat = 0
|
sat = 0
|
||||||
hue = parsed[STATE_HUE]
|
|
||||||
|
|
||||||
# Convert hs values to hass hs values
|
# Convert hs values to hass hs values
|
||||||
sat = int((sat / HUE_API_STATE_SAT_MAX) * 100)
|
|
||||||
hue = int((hue / HUE_API_STATE_HUE_MAX) * 360)
|
hue = int((hue / HUE_API_STATE_HUE_MAX) * 360)
|
||||||
|
sat = int((sat / HUE_API_STATE_SAT_MAX) * 100)
|
||||||
|
|
||||||
data[ATTR_HS_COLOR] = (hue, sat)
|
data[ATTR_HS_COLOR] = (hue, sat)
|
||||||
|
|
||||||
# If the requested entity is a script add some variables
|
if entity_features & SUPPORT_COLOR_TEMP:
|
||||||
|
if parsed[STATE_COLOR_TEMP] is not None:
|
||||||
|
data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP]
|
||||||
|
|
||||||
|
# If the requested entity is a script, add some variables
|
||||||
elif entity.domain == script.DOMAIN:
|
elif entity.domain == script.DOMAIN:
|
||||||
data["variables"] = {
|
data["variables"] = {
|
||||||
"requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF
|
"requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF
|
||||||
@ -366,8 +432,8 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
elif 66.6 < brightness <= 100:
|
elif 66.6 < brightness <= 100:
|
||||||
data[ATTR_SPEED] = SPEED_HIGH
|
data[ATTR_SPEED] = SPEED_HIGH
|
||||||
|
|
||||||
|
# Map the off command to on
|
||||||
if entity.domain in config.off_maps_to_on_domains:
|
if entity.domain in config.off_maps_to_on_domains:
|
||||||
# Map the off command to on
|
|
||||||
service = SERVICE_TURN_ON
|
service = SERVICE_TURN_ON
|
||||||
|
|
||||||
# Caching is required because things like scripts and scenes won't
|
# Caching is required because things like scripts and scenes won't
|
||||||
@ -393,141 +459,65 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
hass.services.async_call(domain, service, data, blocking=True)
|
hass.services.async_call(domain, service, data, blocking=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create success responses for all received keys
|
||||||
json_response = [
|
json_response = [
|
||||||
create_hue_success_response(entity_id, HUE_API_STATE_ON, parsed[STATE_ON])
|
create_hue_success_response(entity_id, HUE_API_STATE_ON, parsed[STATE_ON])
|
||||||
]
|
]
|
||||||
|
|
||||||
if parsed[STATE_BRIGHTNESS] is not None:
|
for (key, val) in (
|
||||||
json_response.append(
|
(STATE_BRIGHTNESS, HUE_API_STATE_BRI),
|
||||||
create_hue_success_response(
|
(STATE_HUE, HUE_API_STATE_HUE),
|
||||||
entity_id, HUE_API_STATE_BRI, parsed[STATE_BRIGHTNESS]
|
(STATE_SATURATION, HUE_API_STATE_SAT),
|
||||||
|
(STATE_COLOR_TEMP, HUE_API_STATE_CT),
|
||||||
|
):
|
||||||
|
if parsed[key] is not None:
|
||||||
|
json_response.append(
|
||||||
|
create_hue_success_response(entity_id, val, parsed[key])
|
||||||
)
|
)
|
||||||
)
|
|
||||||
if parsed[STATE_HUE] is not None:
|
|
||||||
json_response.append(
|
|
||||||
create_hue_success_response(
|
|
||||||
entity_id, HUE_API_STATE_HUE, parsed[STATE_HUE]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if parsed[STATE_SATURATION] is not None:
|
|
||||||
json_response.append(
|
|
||||||
create_hue_success_response(
|
|
||||||
entity_id, HUE_API_STATE_SAT, parsed[STATE_SATURATION]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
|
|
||||||
def parse_hue_api_put_light_body(request_json, entity):
|
|
||||||
"""Parse the body of a request to change the state of a light."""
|
|
||||||
data = {
|
|
||||||
STATE_BRIGHTNESS: None,
|
|
||||||
STATE_HUE: None,
|
|
||||||
STATE_ON: False,
|
|
||||||
STATE_SATURATION: None,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Make sure the entity actually supports brightness
|
|
||||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
||||||
|
|
||||||
if HUE_API_STATE_ON in request_json:
|
|
||||||
if not isinstance(request_json[HUE_API_STATE_ON], bool):
|
|
||||||
return None
|
|
||||||
|
|
||||||
if request_json[HUE_API_STATE_ON]:
|
|
||||||
# Echo requested device be turned on
|
|
||||||
data[STATE_BRIGHTNESS] = None
|
|
||||||
data[STATE_ON] = True
|
|
||||||
else:
|
|
||||||
# Echo requested device be turned off
|
|
||||||
data[STATE_BRIGHTNESS] = None
|
|
||||||
data[STATE_ON] = False
|
|
||||||
|
|
||||||
if HUE_API_STATE_HUE in request_json:
|
|
||||||
try:
|
|
||||||
# Clamp brightness from 0 to 65535
|
|
||||||
data[STATE_HUE] = max(
|
|
||||||
0, min(int(request_json[HUE_API_STATE_HUE]), HUE_API_STATE_HUE_MAX)
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if HUE_API_STATE_SAT in request_json:
|
|
||||||
try:
|
|
||||||
# Clamp saturation from 0 to 254
|
|
||||||
data[STATE_SATURATION] = max(
|
|
||||||
0, min(int(request_json[HUE_API_STATE_SAT]), HUE_API_STATE_SAT_MAX)
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if HUE_API_STATE_BRI in request_json:
|
|
||||||
try:
|
|
||||||
# Clamp brightness from 0 to 255
|
|
||||||
data[STATE_BRIGHTNESS] = max(
|
|
||||||
0, min(int(request_json[HUE_API_STATE_BRI]), HUE_API_STATE_BRI_MAX)
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if entity.domain == light.DOMAIN:
|
|
||||||
data[STATE_ON] = data[STATE_BRIGHTNESS] > 0
|
|
||||||
if not entity_features & SUPPORT_BRIGHTNESS:
|
|
||||||
data[STATE_BRIGHTNESS] = None
|
|
||||||
|
|
||||||
elif entity.domain == scene.DOMAIN:
|
|
||||||
data[STATE_BRIGHTNESS] = None
|
|
||||||
data[STATE_ON] = True
|
|
||||||
|
|
||||||
elif entity.domain in [
|
|
||||||
script.DOMAIN,
|
|
||||||
media_player.DOMAIN,
|
|
||||||
fan.DOMAIN,
|
|
||||||
cover.DOMAIN,
|
|
||||||
climate.DOMAIN,
|
|
||||||
]:
|
|
||||||
# Convert 0-255 to 0-100
|
|
||||||
level = (data[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100
|
|
||||||
data[STATE_BRIGHTNESS] = round(level)
|
|
||||||
data[STATE_ON] = True
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def get_entity_state(config, entity):
|
def get_entity_state(config, entity):
|
||||||
"""Retrieve and convert state and brightness values for an entity."""
|
"""Retrieve and convert state and brightness values for an entity."""
|
||||||
cached_state = config.cached_states.get(entity.entity_id, None)
|
cached_state = config.cached_states.get(entity.entity_id, None)
|
||||||
data = {
|
data = {
|
||||||
|
STATE_ON: False,
|
||||||
STATE_BRIGHTNESS: None,
|
STATE_BRIGHTNESS: None,
|
||||||
STATE_HUE: None,
|
STATE_HUE: None,
|
||||||
STATE_ON: False,
|
|
||||||
STATE_SATURATION: None,
|
STATE_SATURATION: None,
|
||||||
|
STATE_COLOR_TEMP: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cached_state is None:
|
if cached_state is None:
|
||||||
data[STATE_ON] = entity.state != STATE_OFF
|
data[STATE_ON] = entity.state != STATE_OFF
|
||||||
|
|
||||||
if data[STATE_ON]:
|
if data[STATE_ON]:
|
||||||
data[STATE_BRIGHTNESS] = entity.attributes.get(ATTR_BRIGHTNESS, 0)
|
data[STATE_BRIGHTNESS] = entity.attributes.get(ATTR_BRIGHTNESS, 0)
|
||||||
hue_sat = entity.attributes.get(ATTR_HS_COLOR, None)
|
hue_sat = entity.attributes.get(ATTR_HS_COLOR, None)
|
||||||
if hue_sat is not None:
|
if hue_sat is not None:
|
||||||
hue = hue_sat[0]
|
hue = hue_sat[0]
|
||||||
sat = hue_sat[1]
|
sat = hue_sat[1]
|
||||||
# convert hass hs values back to hue hs values
|
# Convert hass hs values back to hue hs values
|
||||||
data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX)
|
data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX)
|
||||||
data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX)
|
data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX)
|
||||||
|
else:
|
||||||
|
data[STATE_HUE] = HUE_API_STATE_HUE_MIN
|
||||||
|
data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN
|
||||||
|
data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
data[STATE_BRIGHTNESS] = 0
|
data[STATE_BRIGHTNESS] = 0
|
||||||
data[STATE_HUE] = 0
|
data[STATE_HUE] = 0
|
||||||
data[STATE_SATURATION] = 0
|
data[STATE_SATURATION] = 0
|
||||||
|
data[STATE_COLOR_TEMP] = 0
|
||||||
|
|
||||||
# Make sure the entity actually supports brightness
|
# Get the entity's supported features
|
||||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
if entity.domain == light.DOMAIN:
|
if entity.domain == light.DOMAIN:
|
||||||
if entity_features & SUPPORT_BRIGHTNESS:
|
if entity_features & SUPPORT_BRIGHTNESS:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif entity.domain == climate.DOMAIN:
|
elif entity.domain == climate.DOMAIN:
|
||||||
temperature = entity.attributes.get(ATTR_TEMPERATURE, 0)
|
temperature = entity.attributes.get(ATTR_TEMPERATURE, 0)
|
||||||
# Convert 0-100 to 0-255
|
# Convert 0-100 to 0-255
|
||||||
@ -537,7 +527,7 @@ def get_entity_state(config, entity):
|
|||||||
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0
|
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0
|
||||||
)
|
)
|
||||||
# Convert 0.0-1.0 to 0-255
|
# Convert 0.0-1.0 to 0-255
|
||||||
data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX)
|
data[STATE_BRIGHTNESS] = round(min(1.0, level) * 255)
|
||||||
elif entity.domain == fan.DOMAIN:
|
elif entity.domain == fan.DOMAIN:
|
||||||
speed = entity.attributes.get(ATTR_SPEED, 0)
|
speed = entity.attributes.get(ATTR_SPEED, 0)
|
||||||
# Convert 0.0-1.0 to 0-255
|
# Convert 0.0-1.0 to 0-255
|
||||||
@ -550,12 +540,13 @@ def get_entity_state(config, entity):
|
|||||||
data[STATE_BRIGHTNESS] = 255
|
data[STATE_BRIGHTNESS] = 255
|
||||||
elif entity.domain == cover.DOMAIN:
|
elif entity.domain == cover.DOMAIN:
|
||||||
level = entity.attributes.get(ATTR_CURRENT_POSITION, 0)
|
level = entity.attributes.get(ATTR_CURRENT_POSITION, 0)
|
||||||
data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX)
|
data[STATE_BRIGHTNESS] = round(level / 100 * 255)
|
||||||
else:
|
else:
|
||||||
data = cached_state
|
data = cached_state
|
||||||
# Make sure brightness is valid
|
# Make sure brightness is valid
|
||||||
if data[STATE_BRIGHTNESS] is None:
|
if data[STATE_BRIGHTNESS] is None:
|
||||||
data[STATE_BRIGHTNESS] = 255 if data[STATE_ON] else 0
|
data[STATE_BRIGHTNESS] = 255 if data[STATE_ON] else 0
|
||||||
|
|
||||||
# Make sure hue/saturation are valid
|
# Make sure hue/saturation are valid
|
||||||
if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None):
|
if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None):
|
||||||
data[STATE_HUE] = 0
|
data[STATE_HUE] = 0
|
||||||
@ -566,39 +557,117 @@ def get_entity_state(config, entity):
|
|||||||
data[STATE_HUE] = 0
|
data[STATE_HUE] = 0
|
||||||
data[STATE_SATURATION] = 0
|
data[STATE_SATURATION] = 0
|
||||||
|
|
||||||
|
# Clamp brightness, hue, saturation, and color temp to valid values
|
||||||
|
for (key, v_min, v_max) in (
|
||||||
|
(STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX),
|
||||||
|
(STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX),
|
||||||
|
(STATE_SATURATION, HUE_API_STATE_SAT_MIN, HUE_API_STATE_SAT_MAX),
|
||||||
|
(STATE_COLOR_TEMP, HUE_API_STATE_CT_MIN, HUE_API_STATE_CT_MAX),
|
||||||
|
):
|
||||||
|
if data[key] is not None:
|
||||||
|
data[key] = max(v_min, min(data[key], v_max))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def entity_to_json(config, entity, state):
|
def entity_to_json(config, entity):
|
||||||
"""Convert an entity to its Hue bridge JSON representation."""
|
"""Convert an entity to its Hue bridge JSON representation."""
|
||||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if (entity_features & SUPPORT_BRIGHTNESS) or entity.domain != light.DOMAIN:
|
unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest()
|
||||||
return {
|
unique_id = "00:{}:{}:{}:{}:{}:{}:{}-{}".format(
|
||||||
"state": {
|
unique_id[0:2],
|
||||||
HUE_API_STATE_ON: state[STATE_ON],
|
unique_id[2:4],
|
||||||
HUE_API_STATE_BRI: state[STATE_BRIGHTNESS],
|
unique_id[4:6],
|
||||||
HUE_API_STATE_HUE: state[STATE_HUE],
|
unique_id[6:8],
|
||||||
HUE_API_STATE_SAT: state[STATE_SATURATION],
|
unique_id[8:10],
|
||||||
"reachable": entity.state != STATE_UNAVAILABLE,
|
unique_id[10:12],
|
||||||
},
|
unique_id[12:14],
|
||||||
"type": "Dimmable light",
|
unique_id[14:16],
|
||||||
"name": config.get_entity_name(entity),
|
)
|
||||||
"modelid": "HASS123",
|
|
||||||
"uniqueid": entity.entity_id,
|
state = get_entity_state(config, entity)
|
||||||
"swversion": "123",
|
|
||||||
}
|
retval = {
|
||||||
return {
|
|
||||||
"state": {
|
"state": {
|
||||||
HUE_API_STATE_ON: state[STATE_ON],
|
HUE_API_STATE_ON: state[STATE_ON],
|
||||||
"reachable": entity.state != STATE_UNAVAILABLE,
|
"reachable": entity.state != STATE_UNAVAILABLE,
|
||||||
|
"mode": "homeautomation",
|
||||||
},
|
},
|
||||||
"type": "On/off light",
|
|
||||||
"name": config.get_entity_name(entity),
|
"name": config.get_entity_name(entity),
|
||||||
"modelid": "HASS321",
|
"uniqueid": unique_id,
|
||||||
"uniqueid": entity.entity_id,
|
"manufacturername": "Home Assistant",
|
||||||
"swversion": "123",
|
"swversion": "123",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(entity_features & SUPPORT_BRIGHTNESS)
|
||||||
|
and (entity_features & SUPPORT_COLOR)
|
||||||
|
and (entity_features & SUPPORT_COLOR_TEMP)
|
||||||
|
):
|
||||||
|
# Extended Color light (ZigBee Device ID: 0x0210)
|
||||||
|
# Same as Color light, but which supports additional setting of color temperature
|
||||||
|
retval["type"] = "Extended color light"
|
||||||
|
retval["modelid"] = "HASS231"
|
||||||
|
retval["state"].update(
|
||||||
|
{
|
||||||
|
HUE_API_STATE_BRI: state[STATE_BRIGHTNESS],
|
||||||
|
HUE_API_STATE_HUE: state[STATE_HUE],
|
||||||
|
HUE_API_STATE_SAT: state[STATE_SATURATION],
|
||||||
|
HUE_API_STATE_CT: state[STATE_COLOR_TEMP],
|
||||||
|
HUE_API_STATE_EFFECT: "none",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if state[STATE_HUE] > 0 or state[STATE_SATURATION] > 0:
|
||||||
|
retval["state"][HUE_API_STATE_COLORMODE] = "hs"
|
||||||
|
else:
|
||||||
|
retval["state"][HUE_API_STATE_COLORMODE] = "ct"
|
||||||
|
elif (entity_features & SUPPORT_BRIGHTNESS) and (entity_features & SUPPORT_COLOR):
|
||||||
|
# Color light (ZigBee Device ID: 0x0200)
|
||||||
|
# Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY)
|
||||||
|
retval["type"] = "Color light"
|
||||||
|
retval["modelid"] = "HASS213"
|
||||||
|
retval["state"].update(
|
||||||
|
{
|
||||||
|
HUE_API_STATE_BRI: state[STATE_BRIGHTNESS],
|
||||||
|
HUE_API_STATE_COLORMODE: "hs",
|
||||||
|
HUE_API_STATE_HUE: state[STATE_HUE],
|
||||||
|
HUE_API_STATE_SAT: state[STATE_SATURATION],
|
||||||
|
HUE_API_STATE_EFFECT: "none",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif (entity_features & SUPPORT_BRIGHTNESS) and (
|
||||||
|
entity_features & SUPPORT_COLOR_TEMP
|
||||||
|
):
|
||||||
|
# Color temperature light (ZigBee Device ID: 0x0220)
|
||||||
|
# Supports groups, scenes, on/off, dimming, and setting of a color temperature
|
||||||
|
retval["type"] = "Color temperature light"
|
||||||
|
retval["modelid"] = "HASS312"
|
||||||
|
retval["state"].update(
|
||||||
|
{HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state[STATE_COLOR_TEMP]}
|
||||||
|
)
|
||||||
|
elif (
|
||||||
|
entity_features
|
||||||
|
& (
|
||||||
|
SUPPORT_BRIGHTNESS
|
||||||
|
| SUPPORT_SET_POSITION
|
||||||
|
| SUPPORT_SET_SPEED
|
||||||
|
| SUPPORT_VOLUME_SET
|
||||||
|
| SUPPORT_TARGET_TEMPERATURE
|
||||||
|
)
|
||||||
|
) or entity.domain == script.DOMAIN:
|
||||||
|
# Dimmable light (ZigBee Device ID: 0x0100)
|
||||||
|
# Supports groups, scenes, on/off and dimming
|
||||||
|
retval["type"] = "Dimmable light"
|
||||||
|
retval["modelid"] = "HASS123"
|
||||||
|
retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]})
|
||||||
|
else:
|
||||||
|
# On/off light (ZigBee Device ID: 0x0000)
|
||||||
|
# Supports groups, scenes and on/off control
|
||||||
|
retval["type"] = "On/off light"
|
||||||
|
retval["modelid"] = "HASS321"
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
def create_hue_success_response(entity_id, attr, value):
|
def create_hue_success_response(entity_id, attr, value):
|
||||||
"""Create a success response for an attribute set on a light."""
|
"""Create a success response for an attribute set on a light."""
|
||||||
|
@ -205,20 +205,20 @@ def test_discover_lights(hue_client):
|
|||||||
devices = set(val["uniqueid"] for val in result_json.values())
|
devices = set(val["uniqueid"] for val in result_json.values())
|
||||||
|
|
||||||
# Make sure the lights we added to the config are there
|
# Make sure the lights we added to the config are there
|
||||||
assert "light.ceiling_lights" in devices
|
assert "00:2f:d2:31:ce:c5:55:cc-ee" in devices # light.ceiling_lights
|
||||||
assert "light.bed_light" not in devices
|
assert "00:b6:14:77:34:b7:bb:06-e8" not in devices # light.bed_light
|
||||||
assert "script.set_kitchen_light" in devices
|
assert "00:95:b7:51:16:58:6c:c0-c5" in devices # script.set_kitchen_light
|
||||||
assert "light.kitchen_lights" not in devices
|
assert "00:64:7b:e4:96:c3:fe:90-c3" not in devices # light.kitchen_lights
|
||||||
assert "media_player.living_room" in devices
|
assert "00:7e:8a:42:35:66:db:86-c5" in devices # media_player.living_room
|
||||||
assert "media_player.bedroom" in devices
|
assert "00:05:44:c2:d6:0a:e5:17-b7" in devices # media_player.bedroom
|
||||||
assert "media_player.walkman" in devices
|
assert "00:f3:5f:fa:31:f3:32:21-a8" in devices # media_player.walkman
|
||||||
assert "media_player.lounge_room" in devices
|
assert "00:b4:06:2e:91:95:23:97-fb" in devices # media_player.lounge_room
|
||||||
assert "fan.living_room_fan" in devices
|
assert "00:b2:bd:f9:2c:ad:22:ae-58" in devices # fan.living_room_fan
|
||||||
assert "fan.ceiling_fan" not in devices
|
assert "00:77:4c:8a:23:7d:27:4b-7f" not in devices # fan.ceiling_fan
|
||||||
assert "cover.living_room_window" in devices
|
assert "00:02:53:b9:d5:1a:b3:67-b2" in devices # cover.living_room_window
|
||||||
assert "climate.hvac" in devices
|
assert "00:42:03:fe:97:58:2d:b1-50" in devices # climate.hvac
|
||||||
assert "climate.heatpump" in devices
|
assert "00:7b:2a:c7:08:d6:66:bf-80" in devices # climate.heatpump
|
||||||
assert "climate.ecobee" not in devices
|
assert "00:57:77:a1:6a:8e:ef:b3-6c" not in devices # climate.ecobee
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@ -300,15 +300,15 @@ def test_get_light_state(hass_hue, hue_client):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert office_json["state"][HUE_API_STATE_ON] is False
|
assert office_json["state"][HUE_API_STATE_ON] is False
|
||||||
assert office_json["state"][HUE_API_STATE_BRI] == 0
|
# Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254
|
||||||
assert office_json["state"][HUE_API_STATE_HUE] == 0
|
assert office_json["state"][HUE_API_STATE_HUE] == 0
|
||||||
assert office_json["state"][HUE_API_STATE_SAT] == 0
|
assert office_json["state"][HUE_API_STATE_SAT] == 0
|
||||||
|
|
||||||
# Make sure bedroom light isn't accessible
|
# Make sure bedroom light isn't accessible
|
||||||
yield from perform_get_light_state(hue_client, "light.bed_light", 404)
|
yield from perform_get_light_state(hue_client, "light.bed_light", 401)
|
||||||
|
|
||||||
# Make sure kitchen light isn't accessible
|
# Make sure kitchen light isn't accessible
|
||||||
yield from perform_get_light_state(hue_client, "light.kitchen_lights", 404)
|
yield from perform_get_light_state(hue_client, "light.kitchen_lights", 401)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@ -365,7 +365,7 @@ def test_put_light_state(hass_hue, hue_client):
|
|||||||
ceiling_json = yield from perform_get_light_state(
|
ceiling_json = yield from perform_get_light_state(
|
||||||
hue_client, "light.ceiling_lights", 200
|
hue_client, "light.ceiling_lights", 200
|
||||||
)
|
)
|
||||||
assert ceiling_json["state"][HUE_API_STATE_BRI] == 0
|
# Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254
|
||||||
assert ceiling_json["state"][HUE_API_STATE_HUE] == 0
|
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_SAT] == 0
|
||||||
|
|
||||||
@ -373,7 +373,7 @@ def test_put_light_state(hass_hue, hue_client):
|
|||||||
bedroom_result = yield from perform_put_light_state(
|
bedroom_result = yield from perform_put_light_state(
|
||||||
hass_hue, hue_client, "light.bed_light", True
|
hass_hue, hue_client, "light.bed_light", True
|
||||||
)
|
)
|
||||||
assert bedroom_result.status == 404
|
assert bedroom_result.status == 401
|
||||||
|
|
||||||
# Make sure we can't change the kitchen light state
|
# Make sure we can't change the kitchen light state
|
||||||
kitchen_result = yield from perform_put_light_state(
|
kitchen_result = yield from perform_put_light_state(
|
||||||
@ -434,7 +434,7 @@ def test_put_light_state_climate_set_temperature(hass_hue, hue_client):
|
|||||||
ecobee_result = yield from perform_put_light_state(
|
ecobee_result = yield from perform_put_light_state(
|
||||||
hass_hue, hue_client, "climate.ecobee", True
|
hass_hue, hue_client, "climate.ecobee", True
|
||||||
)
|
)
|
||||||
assert ecobee_result.status == 404
|
assert ecobee_result.status == 401
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
@ -769,4 +769,4 @@ async def test_external_ip_blocked(hue_client):
|
|||||||
):
|
):
|
||||||
result = await hue_client.get("/api/username/lights")
|
result = await hue_client.get("/api/username/lights")
|
||||||
|
|
||||||
assert result.status == 400
|
assert result.status == 401
|
||||||
|
Loading…
x
Reference in New Issue
Block a user