2019-02-13 20:21:14 +00:00
|
|
|
"""Support for a Hue API to control Home Assistant."""
|
2016-12-04 18:57:48 +00:00
|
|
|
import logging
|
|
|
|
|
|
|
|
from aiohttp import web
|
|
|
|
|
|
|
|
from homeassistant import core
|
2019-04-02 18:25:58 +00:00
|
|
|
from homeassistant.components import (
|
2019-07-31 19:25:30 +00:00
|
|
|
climate,
|
|
|
|
cover,
|
|
|
|
fan,
|
|
|
|
light,
|
|
|
|
media_player,
|
|
|
|
scene,
|
|
|
|
script,
|
|
|
|
)
|
2019-03-10 23:04:21 +00:00
|
|
|
from homeassistant.components.climate.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
SERVICE_SET_TEMPERATURE,
|
|
|
|
SUPPORT_TARGET_TEMPERATURE,
|
|
|
|
)
|
2019-02-11 18:59:34 +00:00
|
|
|
from homeassistant.components.cover import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_CURRENT_POSITION,
|
|
|
|
ATTR_POSITION,
|
|
|
|
SERVICE_SET_COVER_POSITION,
|
|
|
|
SUPPORT_SET_POSITION,
|
|
|
|
)
|
2019-04-02 18:25:58 +00:00
|
|
|
from homeassistant.components.fan import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_SPEED,
|
|
|
|
SPEED_HIGH,
|
|
|
|
SPEED_LOW,
|
|
|
|
SPEED_MEDIUM,
|
|
|
|
SPEED_OFF,
|
|
|
|
SUPPORT_SET_SPEED,
|
|
|
|
)
|
2016-12-04 18:57:48 +00:00
|
|
|
from homeassistant.components.http import HomeAssistantView
|
2018-10-08 18:50:24 +00:00
|
|
|
from homeassistant.components.http.const import KEY_REAL_IP
|
2019-04-02 18:25:58 +00:00
|
|
|
from homeassistant.components.light import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_BRIGHTNESS,
|
|
|
|
ATTR_HS_COLOR,
|
|
|
|
SUPPORT_BRIGHTNESS,
|
|
|
|
SUPPORT_COLOR,
|
|
|
|
)
|
2019-04-02 18:25:58 +00:00
|
|
|
from homeassistant.components.media_player.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_MEDIA_VOLUME_LEVEL,
|
|
|
|
SUPPORT_VOLUME_SET,
|
|
|
|
)
|
2019-04-02 18:25:58 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_ENTITY_ID,
|
|
|
|
ATTR_SUPPORTED_FEATURES,
|
|
|
|
ATTR_TEMPERATURE,
|
|
|
|
HTTP_BAD_REQUEST,
|
|
|
|
HTTP_NOT_FOUND,
|
|
|
|
SERVICE_CLOSE_COVER,
|
|
|
|
SERVICE_OPEN_COVER,
|
|
|
|
SERVICE_TURN_OFF,
|
|
|
|
SERVICE_TURN_ON,
|
|
|
|
SERVICE_VOLUME_SET,
|
|
|
|
STATE_OFF,
|
|
|
|
STATE_ON,
|
|
|
|
)
|
2018-10-08 18:50:24 +00:00
|
|
|
from homeassistant.util.network import is_local
|
|
|
|
|
2016-12-04 18:57:48 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
HUE_API_STATE_ON = "on"
|
|
|
|
HUE_API_STATE_BRI = "bri"
|
|
|
|
HUE_API_STATE_HUE = "hue"
|
|
|
|
HUE_API_STATE_SAT = "sat"
|
2019-04-02 18:25:58 +00:00
|
|
|
|
|
|
|
HUE_API_STATE_HUE_MAX = 65535.0
|
|
|
|
HUE_API_STATE_SAT_MAX = 254.0
|
|
|
|
HUE_API_STATE_BRI_MAX = 255.0
|
|
|
|
|
|
|
|
STATE_BRIGHTNESS = HUE_API_STATE_BRI
|
|
|
|
STATE_HUE = HUE_API_STATE_HUE
|
|
|
|
STATE_SATURATION = HUE_API_STATE_SAT
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HueUsernameView(HomeAssistantView):
|
|
|
|
"""Handle requests to create a username for the emulated hue bridge."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api"
|
|
|
|
name = "emulated_hue:api:create_username"
|
|
|
|
extra_urls = ["/api/"]
|
2016-12-04 18:57:48 +00:00
|
|
|
requires_auth = False
|
|
|
|
|
2018-10-01 06:52:42 +00:00
|
|
|
async def post(self, request):
|
2016-12-04 18:57:48 +00:00
|
|
|
"""Handle a POST request."""
|
|
|
|
try:
|
2018-10-01 06:52:42 +00:00
|
|
|
data = await request.json()
|
2016-12-04 18:57:48 +00:00
|
|
|
except ValueError:
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if "devicetype" not in data:
|
|
|
|
return self.json_message("devicetype not specified", HTTP_BAD_REQUEST)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2018-10-08 18:50:24 +00:00
|
|
|
if not is_local(request[KEY_REAL_IP]):
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
2018-10-08 18:50:24 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json([{"success": {"username": "12345678901234567890"}}])
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
|
2019-01-29 17:26:07 +00:00
|
|
|
class HueAllGroupsStateView(HomeAssistantView):
|
|
|
|
"""Group handler."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api/{username}/groups"
|
|
|
|
name = "emulated_hue:all_groups:state"
|
2019-01-29 17:26:07 +00:00
|
|
|
requires_auth = False
|
|
|
|
|
|
|
|
def __init__(self, config):
|
|
|
|
"""Initialize the instance of the view."""
|
|
|
|
self.config = config
|
|
|
|
|
|
|
|
@core.callback
|
|
|
|
def get(self, request, username):
|
|
|
|
"""Process a request to make the Brilliant Lightpad work."""
|
|
|
|
if not is_local(request[KEY_REAL_IP]):
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
2019-01-29 17:26:07 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json({})
|
2019-01-29 17:26:07 +00:00
|
|
|
|
|
|
|
|
2018-04-28 18:39:21 +00:00
|
|
|
class HueGroupView(HomeAssistantView):
|
|
|
|
"""Group handler to get Logitech Pop working."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api/{username}/groups/0/action"
|
|
|
|
name = "emulated_hue:groups:state"
|
2018-04-28 18:39:21 +00:00
|
|
|
requires_auth = False
|
|
|
|
|
|
|
|
def __init__(self, config):
|
|
|
|
"""Initialize the instance of the view."""
|
|
|
|
self.config = config
|
|
|
|
|
|
|
|
@core.callback
|
|
|
|
def put(self, request, username):
|
|
|
|
"""Process a request to make the Logitech Pop working."""
|
2018-10-08 18:50:24 +00:00
|
|
|
if not is_local(request[KEY_REAL_IP]):
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
|
|
|
|
|
|
|
return self.json(
|
|
|
|
[
|
|
|
|
{
|
|
|
|
"error": {
|
|
|
|
"address": "/groups/0/action/scene",
|
|
|
|
"type": 7,
|
|
|
|
"description": "invalid value, dummy for parameter, scene",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
)
|
2018-04-28 18:39:21 +00:00
|
|
|
|
|
|
|
|
2016-12-04 18:57:48 +00:00
|
|
|
class HueAllLightsStateView(HomeAssistantView):
|
|
|
|
"""Handle requests for getting and setting info about entities."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api/{username}/lights"
|
|
|
|
name = "emulated_hue:lights:state"
|
2016-12-04 18:57:48 +00:00
|
|
|
requires_auth = False
|
|
|
|
|
|
|
|
def __init__(self, config):
|
|
|
|
"""Initialize the instance of the view."""
|
|
|
|
self.config = config
|
|
|
|
|
|
|
|
@core.callback
|
|
|
|
def get(self, request, username):
|
|
|
|
"""Process a request to get the list of available lights."""
|
2018-10-08 18:50:24 +00:00
|
|
|
if not is_local(request[KEY_REAL_IP]):
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
2018-10-08 18:50:24 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2016-12-04 18:57:48 +00:00
|
|
|
json_response = {}
|
|
|
|
|
|
|
|
for entity in hass.states.async_all():
|
|
|
|
if self.config.is_entity_exposed(entity):
|
2019-04-02 18:25:58 +00:00
|
|
|
state = get_entity_state(self.config, entity)
|
2016-12-16 05:47:23 +00:00
|
|
|
|
2016-12-04 18:57:48 +00:00
|
|
|
number = self.config.entity_id_to_number(entity.entity_id)
|
2019-07-31 19:25:30 +00:00
|
|
|
json_response[number] = entity_to_json(self.config, entity, state)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
return self.json(json_response)
|
|
|
|
|
|
|
|
|
|
|
|
class HueOneLightStateView(HomeAssistantView):
|
|
|
|
"""Handle requests for getting and setting info about entities."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api/{username}/lights/{entity_id}"
|
|
|
|
name = "emulated_hue:light:state"
|
2016-12-04 18:57:48 +00:00
|
|
|
requires_auth = False
|
|
|
|
|
|
|
|
def __init__(self, config):
|
|
|
|
"""Initialize the instance of the view."""
|
|
|
|
self.config = config
|
|
|
|
|
|
|
|
@core.callback
|
2017-01-11 20:25:02 +00:00
|
|
|
def get(self, request, username, entity_id):
|
2016-12-04 18:57:48 +00:00
|
|
|
"""Process a request to get the state of an individual light."""
|
2018-10-08 18:50:24 +00:00
|
|
|
if not is_local(request[KEY_REAL_IP]):
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
2018-10-08 18:50:24 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2016-12-04 18:57:48 +00:00
|
|
|
entity_id = self.config.number_to_entity_id(entity_id)
|
|
|
|
entity = hass.states.get(entity_id)
|
|
|
|
|
|
|
|
if entity is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Entity not found: %s", entity_id)
|
2016-12-04 18:57:48 +00:00
|
|
|
return web.Response(text="Entity not found", status=404)
|
|
|
|
|
|
|
|
if not self.config.is_entity_exposed(entity):
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Entity not exposed: %s", entity_id)
|
2016-12-04 18:57:48 +00:00
|
|
|
return web.Response(text="Entity not exposed", status=404)
|
|
|
|
|
2019-04-02 18:25:58 +00:00
|
|
|
state = get_entity_state(self.config, entity)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2019-04-02 18:25:58 +00:00
|
|
|
json_response = entity_to_json(self.config, entity, state)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
return self.json(json_response)
|
|
|
|
|
|
|
|
|
|
|
|
class HueOneLightChangeView(HomeAssistantView):
|
|
|
|
"""Handle requests for getting and setting info about entities."""
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
url = "/api/{username}/lights/{entity_number}/state"
|
|
|
|
name = "emulated_hue:light:state"
|
2016-12-04 18:57:48 +00:00
|
|
|
requires_auth = False
|
|
|
|
|
|
|
|
def __init__(self, config):
|
|
|
|
"""Initialize the instance of the view."""
|
|
|
|
self.config = config
|
|
|
|
|
2018-10-01 06:52:42 +00:00
|
|
|
async def put(self, request, username, entity_number):
|
2016-12-04 18:57:48 +00:00
|
|
|
"""Process a request to set the state of an individual light."""
|
2018-10-08 18:50:24 +00:00
|
|
|
if not is_local(request[KEY_REAL_IP]):
|
2019-07-31 19:25:30 +00:00
|
|
|
return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST)
|
2018-10-08 18:50:24 +00:00
|
|
|
|
2016-12-04 18:57:48 +00:00
|
|
|
config = self.config
|
2019-07-31 19:25:30 +00:00
|
|
|
hass = request.app["hass"]
|
2016-12-04 18:57:48 +00:00
|
|
|
entity_id = config.number_to_entity_id(entity_number)
|
|
|
|
|
|
|
|
if entity_id is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Unknown entity number: %s", entity_number)
|
|
|
|
return self.json_message("Entity not found", HTTP_NOT_FOUND)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
entity = hass.states.get(entity_id)
|
|
|
|
|
|
|
|
if entity is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Entity not found: %s", entity_id)
|
|
|
|
return self.json_message("Entity not found", HTTP_NOT_FOUND)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
if not config.is_entity_exposed(entity):
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Entity not exposed: %s", entity_id)
|
2016-12-04 18:57:48 +00:00
|
|
|
return web.Response(text="Entity not exposed", status=404)
|
|
|
|
|
|
|
|
try:
|
2018-10-01 06:52:42 +00:00
|
|
|
request_json = await request.json()
|
2016-12-04 18:57:48 +00:00
|
|
|
except ValueError:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Received invalid json")
|
|
|
|
return self.json_message("Invalid JSON", HTTP_BAD_REQUEST)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
# Parse the request into requested "on" status and brightness
|
|
|
|
parsed = parse_hue_api_put_light_body(request_json, entity)
|
|
|
|
|
|
|
|
if parsed is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Unable to parse data: %s", request_json)
|
2016-12-04 18:57:48 +00:00
|
|
|
return web.Response(text="Bad request", status=400)
|
|
|
|
|
2016-12-16 05:47:23 +00:00
|
|
|
# Choose general HA domain
|
|
|
|
domain = core.DOMAIN
|
|
|
|
|
2016-12-26 13:00:43 +00:00
|
|
|
# Entity needs separate call to turn on
|
|
|
|
turn_on_needed = False
|
|
|
|
|
2016-12-04 18:57:48 +00:00
|
|
|
# Convert the resulting "on" status into the service we need to call
|
2019-04-02 18:25:58 +00:00
|
|
|
service = SERVICE_TURN_ON if parsed[STATE_ON] else SERVICE_TURN_OFF
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
# Construct what we need to send to the service
|
|
|
|
data = {ATTR_ENTITY_ID: entity_id}
|
|
|
|
|
2016-12-16 05:47:23 +00:00
|
|
|
# Make sure the entity actually supports brightness
|
|
|
|
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
|
2019-02-11 18:59:34 +00:00
|
|
|
if entity.domain == light.DOMAIN:
|
2019-04-02 18:25:58 +00:00
|
|
|
if parsed[STATE_ON]:
|
|
|
|
if entity_features & SUPPORT_BRIGHTNESS:
|
|
|
|
if parsed[STATE_BRIGHTNESS] is not None:
|
|
|
|
data[ATTR_BRIGHTNESS] = parsed[STATE_BRIGHTNESS]
|
|
|
|
if entity_features & SUPPORT_COLOR:
|
|
|
|
if parsed[STATE_HUE] is not None:
|
|
|
|
if parsed[STATE_SATURATION]:
|
|
|
|
sat = parsed[STATE_SATURATION]
|
|
|
|
else:
|
|
|
|
sat = 0
|
|
|
|
hue = parsed[STATE_HUE]
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
data[ATTR_HS_COLOR] = (hue, sat)
|
2016-12-16 05:47:23 +00:00
|
|
|
|
2016-12-04 18:57:48 +00:00
|
|
|
# If the requested entity is a script add some variables
|
2019-02-11 18:59:34 +00:00
|
|
|
elif entity.domain == script.DOMAIN:
|
2019-07-31 19:25:30 +00:00
|
|
|
data["variables"] = {
|
|
|
|
"requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF
|
2016-12-04 18:57:48 +00:00
|
|
|
}
|
|
|
|
|
2019-04-02 18:25:58 +00:00
|
|
|
if parsed[STATE_BRIGHTNESS] is not None:
|
2019-07-31 19:25:30 +00:00
|
|
|
data["variables"]["requested_level"] = parsed[STATE_BRIGHTNESS]
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2019-03-10 23:04:21 +00:00
|
|
|
# If the requested entity is a climate, set the temperature
|
|
|
|
elif entity.domain == climate.DOMAIN:
|
|
|
|
# We don't support turning climate devices on or off,
|
|
|
|
# only setting the temperature
|
|
|
|
service = None
|
|
|
|
|
|
|
|
if entity_features & SUPPORT_TARGET_TEMPERATURE:
|
2019-04-02 18:25:58 +00:00
|
|
|
if parsed[STATE_BRIGHTNESS] is not None:
|
2019-03-10 23:04:21 +00:00
|
|
|
domain = entity.domain
|
|
|
|
service = SERVICE_SET_TEMPERATURE
|
2019-04-02 18:25:58 +00:00
|
|
|
data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS]
|
2019-03-10 23:04:21 +00:00
|
|
|
|
2016-12-16 05:47:23 +00:00
|
|
|
# If the requested entity is a media player, convert to volume
|
2019-02-11 18:59:34 +00:00
|
|
|
elif entity.domain == media_player.DOMAIN:
|
2017-02-08 04:42:45 +00:00
|
|
|
if entity_features & SUPPORT_VOLUME_SET:
|
2019-04-02 18:25:58 +00:00
|
|
|
if parsed[STATE_BRIGHTNESS] is not None:
|
2016-12-26 13:00:43 +00:00
|
|
|
turn_on_needed = True
|
2016-12-16 05:47:23 +00:00
|
|
|
domain = entity.domain
|
|
|
|
service = SERVICE_VOLUME_SET
|
|
|
|
# Convert 0-100 to 0.0-1.0
|
2019-07-31 19:25:30 +00:00
|
|
|
data[ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2016-12-26 13:00:43 +00:00
|
|
|
# If the requested entity is a cover, convert to open_cover/close_cover
|
2019-02-11 18:59:34 +00:00
|
|
|
elif entity.domain == cover.DOMAIN:
|
2016-12-26 13:00:43 +00:00
|
|
|
domain = entity.domain
|
|
|
|
if service == SERVICE_TURN_ON:
|
|
|
|
service = SERVICE_OPEN_COVER
|
|
|
|
else:
|
|
|
|
service = SERVICE_CLOSE_COVER
|
|
|
|
|
2019-02-11 18:59:34 +00:00
|
|
|
if entity_features & SUPPORT_SET_POSITION:
|
2019-04-02 18:25:58 +00:00
|
|
|
if parsed[STATE_BRIGHTNESS] is not None:
|
2019-02-11 18:59:34 +00:00
|
|
|
domain = entity.domain
|
|
|
|
service = SERVICE_SET_COVER_POSITION
|
2019-04-02 18:25:58 +00:00
|
|
|
data[ATTR_POSITION] = parsed[STATE_BRIGHTNESS]
|
2019-02-11 18:59:34 +00:00
|
|
|
|
2017-01-21 06:21:28 +00:00
|
|
|
# If the requested entity is a fan, convert to speed
|
2019-02-11 18:59:34 +00:00
|
|
|
elif entity.domain == fan.DOMAIN:
|
2017-02-08 04:42:45 +00:00
|
|
|
if entity_features & SUPPORT_SET_SPEED:
|
2019-04-02 18:25:58 +00:00
|
|
|
if parsed[STATE_BRIGHTNESS] is not None:
|
2017-01-21 06:21:28 +00:00
|
|
|
domain = entity.domain
|
|
|
|
# Convert 0-100 to a fan speed
|
2019-04-02 18:25:58 +00:00
|
|
|
brightness = parsed[STATE_BRIGHTNESS]
|
2017-01-21 06:21:28 +00:00
|
|
|
if brightness == 0:
|
|
|
|
data[ATTR_SPEED] = SPEED_OFF
|
2018-07-18 09:54:27 +00:00
|
|
|
elif 0 < brightness <= 33.3:
|
2017-01-21 06:21:28 +00:00
|
|
|
data[ATTR_SPEED] = SPEED_LOW
|
2018-07-18 09:54:27 +00:00
|
|
|
elif 33.3 < brightness <= 66.6:
|
2017-01-21 06:21:28 +00:00
|
|
|
data[ATTR_SPEED] = SPEED_MEDIUM
|
2018-07-18 09:54:27 +00:00
|
|
|
elif 66.6 < brightness <= 100:
|
2017-01-21 06:21:28 +00:00
|
|
|
data[ATTR_SPEED] = SPEED_HIGH
|
|
|
|
|
2016-12-04 18:57:48 +00:00
|
|
|
if entity.domain in config.off_maps_to_on_domains:
|
|
|
|
# Map the off command to on
|
|
|
|
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.
|
2019-04-02 18:25:58 +00:00
|
|
|
config.cached_states[entity_id] = parsed
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2016-12-16 05:47:23 +00:00
|
|
|
# Separate call to turn on needed
|
2016-12-26 13:00:43 +00:00
|
|
|
if turn_on_needed:
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.async_create_task(
|
|
|
|
hass.services.async_call(
|
|
|
|
core.DOMAIN,
|
|
|
|
SERVICE_TURN_ON,
|
|
|
|
{ATTR_ENTITY_ID: entity_id},
|
|
|
|
blocking=True,
|
|
|
|
)
|
|
|
|
)
|
2016-12-16 05:47:23 +00:00
|
|
|
|
2019-03-10 23:04:21 +00:00
|
|
|
if service is not None:
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.async_create_task(
|
|
|
|
hass.services.async_call(domain, service, data, blocking=True)
|
|
|
|
)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
json_response = [
|
|
|
|
create_hue_success_response(entity_id, HUE_API_STATE_ON, parsed[STATE_ON])
|
|
|
|
]
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2019-04-02 18:25:58 +00:00
|
|
|
if parsed[STATE_BRIGHTNESS] is not None:
|
2019-07-31 19:25:30 +00:00
|
|
|
json_response.append(
|
|
|
|
create_hue_success_response(
|
|
|
|
entity_id, HUE_API_STATE_BRI, parsed[STATE_BRIGHTNESS]
|
|
|
|
)
|
|
|
|
)
|
2019-04-02 18:25:58 +00:00
|
|
|
if parsed[STATE_HUE] is not None:
|
2019-07-31 19:25:30 +00:00
|
|
|
json_response.append(
|
|
|
|
create_hue_success_response(
|
|
|
|
entity_id, HUE_API_STATE_HUE, parsed[STATE_HUE]
|
|
|
|
)
|
|
|
|
)
|
2019-04-02 18:25:58 +00:00
|
|
|
if parsed[STATE_SATURATION] is not None:
|
2019-07-31 19:25:30 +00:00
|
|
|
json_response.append(
|
|
|
|
create_hue_success_response(
|
|
|
|
entity_id, HUE_API_STATE_SAT, parsed[STATE_SATURATION]
|
|
|
|
)
|
|
|
|
)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
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."""
|
2019-04-02 18:25:58 +00:00
|
|
|
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)
|
|
|
|
|
2016-12-04 18:57:48 +00:00
|
|
|
if HUE_API_STATE_ON in request_json:
|
|
|
|
if not isinstance(request_json[HUE_API_STATE_ON], bool):
|
|
|
|
return None
|
|
|
|
|
2019-04-02 18:25:58 +00:00
|
|
|
if request_json[HUE_API_STATE_ON]:
|
2016-12-04 18:57:48 +00:00
|
|
|
# Echo requested device be turned on
|
2019-04-02 18:25:58 +00:00
|
|
|
data[STATE_BRIGHTNESS] = None
|
|
|
|
data[STATE_ON] = True
|
2016-12-04 18:57:48 +00:00
|
|
|
else:
|
|
|
|
# Echo requested device be turned off
|
2019-04-02 18:25:58 +00:00
|
|
|
data[STATE_BRIGHTNESS] = None
|
|
|
|
data[STATE_ON] = False
|
|
|
|
|
|
|
|
if HUE_API_STATE_HUE in request_json:
|
|
|
|
try:
|
|
|
|
# Clamp brightness from 0 to 65535
|
2019-07-31 19:25:30 +00:00
|
|
|
data[STATE_HUE] = max(
|
|
|
|
0, min(int(request_json[HUE_API_STATE_HUE]), HUE_API_STATE_HUE_MAX)
|
|
|
|
)
|
2019-04-02 18:25:58 +00:00
|
|
|
except ValueError:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if HUE_API_STATE_SAT in request_json:
|
|
|
|
try:
|
|
|
|
# Clamp saturation from 0 to 254
|
2019-07-31 19:25:30 +00:00
|
|
|
data[STATE_SATURATION] = max(
|
|
|
|
0, min(int(request_json[HUE_API_STATE_SAT]), HUE_API_STATE_SAT_MAX)
|
|
|
|
)
|
2019-04-02 18:25:58 +00:00
|
|
|
except ValueError:
|
|
|
|
return None
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
if HUE_API_STATE_BRI in request_json:
|
2016-12-16 05:47:23 +00:00
|
|
|
try:
|
|
|
|
# Clamp brightness from 0 to 255
|
2019-07-31 19:25:30 +00:00
|
|
|
data[STATE_BRIGHTNESS] = max(
|
|
|
|
0, min(int(request_json[HUE_API_STATE_BRI]), HUE_API_STATE_BRI_MAX)
|
|
|
|
)
|
2016-12-16 05:47:23 +00:00
|
|
|
except ValueError:
|
|
|
|
return None
|
|
|
|
|
2019-02-11 18:59:34 +00:00
|
|
|
if entity.domain == light.DOMAIN:
|
2019-07-31 19:25:30 +00:00
|
|
|
data[STATE_ON] = data[STATE_BRIGHTNESS] > 0
|
2019-04-02 18:25:58 +00:00
|
|
|
if not entity_features & SUPPORT_BRIGHTNESS:
|
|
|
|
data[STATE_BRIGHTNESS] = None
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2019-02-11 18:59:34 +00:00
|
|
|
elif entity.domain == scene.DOMAIN:
|
2019-04-02 18:25:58 +00:00
|
|
|
data[STATE_BRIGHTNESS] = None
|
|
|
|
data[STATE_ON] = True
|
2017-11-02 11:02:03 +00:00
|
|
|
|
2019-02-11 18:59:34 +00:00
|
|
|
elif entity.domain in [
|
2019-07-31 19:25:30 +00:00
|
|
|
script.DOMAIN,
|
|
|
|
media_player.DOMAIN,
|
|
|
|
fan.DOMAIN,
|
|
|
|
cover.DOMAIN,
|
|
|
|
climate.DOMAIN,
|
|
|
|
]:
|
2016-12-16 05:47:23 +00:00
|
|
|
# Convert 0-255 to 0-100
|
2019-04-02 18:25:58 +00:00
|
|
|
level = (data[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100
|
|
|
|
data[STATE_BRIGHTNESS] = round(level)
|
|
|
|
data[STATE_ON] = True
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2019-04-02 18:25:58 +00:00
|
|
|
return data
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
|
2016-12-16 05:47:23 +00:00
|
|
|
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)
|
2019-04-02 18:25:58 +00:00
|
|
|
data = {
|
|
|
|
STATE_BRIGHTNESS: None,
|
|
|
|
STATE_HUE: None,
|
|
|
|
STATE_ON: False,
|
2019-07-31 19:25:30 +00:00
|
|
|
STATE_SATURATION: None,
|
2019-04-02 18:25:58 +00:00
|
|
|
}
|
2016-12-16 05:47:23 +00:00
|
|
|
|
|
|
|
if cached_state is None:
|
2019-04-02 18:25:58 +00:00
|
|
|
data[STATE_ON] = entity.state != STATE_OFF
|
|
|
|
if data[STATE_ON]:
|
2019-05-25 20:07:23 +00:00
|
|
|
data[STATE_BRIGHTNESS] = entity.attributes.get(ATTR_BRIGHTNESS, 0)
|
2019-04-02 18:25:58 +00:00
|
|
|
hue_sat = entity.attributes.get(ATTR_HS_COLOR, None)
|
|
|
|
if hue_sat is not None:
|
|
|
|
hue = hue_sat[0]
|
|
|
|
sat = hue_sat[1]
|
|
|
|
# convert hass hs values back to hue hs values
|
|
|
|
data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX)
|
2019-07-31 19:25:30 +00:00
|
|
|
data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX)
|
2019-04-02 18:25:58 +00:00
|
|
|
else:
|
|
|
|
data[STATE_BRIGHTNESS] = 0
|
|
|
|
data[STATE_HUE] = 0
|
|
|
|
data[STATE_SATURATION] = 0
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2016-12-16 05:47:23 +00:00
|
|
|
# Make sure the entity actually supports brightness
|
|
|
|
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
2016-12-04 18:57:48 +00:00
|
|
|
|
2019-02-11 18:59:34 +00:00
|
|
|
if entity.domain == light.DOMAIN:
|
2017-02-08 04:42:45 +00:00
|
|
|
if entity_features & SUPPORT_BRIGHTNESS:
|
|
|
|
pass
|
2016-12-16 05:47:23 +00:00
|
|
|
|
2019-03-10 23:04:21 +00:00
|
|
|
elif entity.domain == climate.DOMAIN:
|
|
|
|
temperature = entity.attributes.get(ATTR_TEMPERATURE, 0)
|
|
|
|
# Convert 0-100 to 0-255
|
2019-04-02 18:25:58 +00:00
|
|
|
data[STATE_BRIGHTNESS] = round(temperature * 255 / 100)
|
2019-02-11 18:59:34 +00:00
|
|
|
elif entity.domain == media_player.DOMAIN:
|
2016-12-16 05:47:23 +00:00
|
|
|
level = entity.attributes.get(
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0
|
|
|
|
)
|
2016-12-16 05:47:23 +00:00
|
|
|
# Convert 0.0-1.0 to 0-255
|
2019-07-31 19:25:30 +00:00
|
|
|
data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX)
|
2019-02-11 18:59:34 +00:00
|
|
|
elif entity.domain == fan.DOMAIN:
|
2017-01-21 06:21:28 +00:00
|
|
|
speed = entity.attributes.get(ATTR_SPEED, 0)
|
|
|
|
# Convert 0.0-1.0 to 0-255
|
2019-04-02 18:25:58 +00:00
|
|
|
data[STATE_BRIGHTNESS] = 0
|
2017-01-21 06:21:28 +00:00
|
|
|
if speed == SPEED_LOW:
|
2019-04-02 18:25:58 +00:00
|
|
|
data[STATE_BRIGHTNESS] = 85
|
2017-01-21 06:21:28 +00:00
|
|
|
elif speed == SPEED_MEDIUM:
|
2019-04-02 18:25:58 +00:00
|
|
|
data[STATE_BRIGHTNESS] = 170
|
2017-01-21 06:21:28 +00:00
|
|
|
elif speed == SPEED_HIGH:
|
2019-04-02 18:25:58 +00:00
|
|
|
data[STATE_BRIGHTNESS] = 255
|
2019-02-11 18:59:34 +00:00
|
|
|
elif entity.domain == cover.DOMAIN:
|
|
|
|
level = entity.attributes.get(ATTR_CURRENT_POSITION, 0)
|
2019-04-02 18:25:58 +00:00
|
|
|
data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX)
|
2016-12-16 05:47:23 +00:00
|
|
|
else:
|
2019-04-02 18:25:58 +00:00
|
|
|
data = cached_state
|
2016-12-26 12:58:32 +00:00
|
|
|
# Make sure brightness is valid
|
2019-04-02 18:25:58 +00:00
|
|
|
if data[STATE_BRIGHTNESS] is None:
|
|
|
|
data[STATE_BRIGHTNESS] = 255 if data[STATE_ON] else 0
|
|
|
|
# Make sure hue/saturation are valid
|
|
|
|
if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None):
|
|
|
|
data[STATE_HUE] = 0
|
|
|
|
data[STATE_SATURATION] = 0
|
|
|
|
|
|
|
|
# If the light is off, set the color to off
|
|
|
|
if data[STATE_BRIGHTNESS] == 0:
|
|
|
|
data[STATE_HUE] = 0
|
|
|
|
data[STATE_SATURATION] = 0
|
2016-12-16 05:47:23 +00:00
|
|
|
|
2019-04-02 18:25:58 +00:00
|
|
|
return data
|
2016-12-16 05:47:23 +00:00
|
|
|
|
|
|
|
|
2019-04-02 18:25:58 +00:00
|
|
|
def entity_to_json(config, entity, state):
|
2016-12-16 05:47:23 +00:00
|
|
|
"""Convert an entity to its Hue bridge JSON representation."""
|
2019-08-21 15:42:26 +00:00
|
|
|
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
if (entity_features & SUPPORT_BRIGHTNESS) or entity.domain != light.DOMAIN:
|
|
|
|
return {
|
|
|
|
"state": {
|
|
|
|
HUE_API_STATE_ON: state[STATE_ON],
|
|
|
|
HUE_API_STATE_BRI: state[STATE_BRIGHTNESS],
|
|
|
|
HUE_API_STATE_HUE: state[STATE_HUE],
|
|
|
|
HUE_API_STATE_SAT: state[STATE_SATURATION],
|
|
|
|
"reachable": True,
|
|
|
|
},
|
|
|
|
"type": "Dimmable light",
|
|
|
|
"name": config.get_entity_name(entity),
|
|
|
|
"modelid": "HASS123",
|
|
|
|
"uniqueid": entity.entity_id,
|
|
|
|
"swversion": "123",
|
|
|
|
}
|
2019-07-31 19:25:30 +00:00
|
|
|
return {
|
2019-08-21 15:42:26 +00:00
|
|
|
"state": {HUE_API_STATE_ON: state[STATE_ON], "reachable": True},
|
|
|
|
"type": "On/off light",
|
2019-07-31 19:25:30 +00:00
|
|
|
"name": config.get_entity_name(entity),
|
2019-08-21 15:42:26 +00:00
|
|
|
"modelid": "HASS321",
|
2019-07-31 19:25:30 +00:00
|
|
|
"uniqueid": entity.entity_id,
|
|
|
|
"swversion": "123",
|
|
|
|
}
|
2016-12-04 18:57:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
def create_hue_success_response(entity_id, attr, value):
|
|
|
|
"""Create a success response for an attribute set on a light."""
|
2019-07-31 19:25:30 +00:00
|
|
|
success_key = "/lights/{}/state/{}".format(entity_id, attr)
|
|
|
|
return {"success": {success_key: value}}
|