mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Expose media volume as emulated Hue brightness (#4869)
* Allow virtual Hue bridge to set volume level of media_player entities * Show correct states in all lights view
This commit is contained in:
parent
394d53e748
commit
7b45cf8e59
@ -6,12 +6,16 @@ from aiohttp import web
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON,
|
||||
STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET,
|
||||
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||
)
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS,
|
||||
SUPPORT_VOLUME_SET,
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -65,8 +69,11 @@ class HueAllLightsStateView(HomeAssistantView):
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
if self.config.is_entity_exposed(entity):
|
||||
state, brightness = get_entity_state(self.config, entity)
|
||||
|
||||
number = self.config.entity_id_to_number(entity.entity_id)
|
||||
json_response[number] = entity_to_json(entity)
|
||||
json_response[number] = entity_to_json(
|
||||
entity, state, brightness)
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
@ -97,16 +104,9 @@ class HueOneLightStateView(HomeAssistantView):
|
||||
_LOGGER.error('Entity not exposed: %s', entity_id)
|
||||
return web.Response(text="Entity not exposed", status=404)
|
||||
|
||||
cached_state = self.config.cached_states.get(entity_id, None)
|
||||
state, brightness = get_entity_state(self.config, entity)
|
||||
|
||||
if cached_state is None:
|
||||
final_state = entity.state == STATE_ON
|
||||
final_brightness = entity.attributes.get(
|
||||
ATTR_BRIGHTNESS, 255 if final_state else 0)
|
||||
else:
|
||||
final_state, final_brightness = cached_state
|
||||
|
||||
json_response = entity_to_json(entity, final_state, final_brightness)
|
||||
json_response = entity_to_json(entity, state, brightness)
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
@ -158,14 +158,24 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
|
||||
result, brightness = parsed
|
||||
|
||||
# Choose general HA domain
|
||||
domain = core.DOMAIN
|
||||
|
||||
# Convert the resulting "on" status into the service we need to call
|
||||
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
|
||||
|
||||
# Construct what we need to send to the service
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
|
||||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
if brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
|
||||
# If the requested entity is a script add some variables
|
||||
if entity.domain == "script":
|
||||
elif entity.domain == "script":
|
||||
data['variables'] = {
|
||||
'requested_state': STATE_ON if result else STATE_OFF
|
||||
}
|
||||
@ -173,8 +183,16 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
if brightness is not None:
|
||||
data['variables']['requested_level'] = brightness
|
||||
|
||||
elif brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
# If the requested entity is a media player, convert to volume
|
||||
elif entity.domain == "media_player":
|
||||
media_commands = entity.attributes.get(
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS, 0)
|
||||
if media_commands & SUPPORT_VOLUME_SET == SUPPORT_VOLUME_SET:
|
||||
if brightness is not None:
|
||||
domain = entity.domain
|
||||
service = SERVICE_VOLUME_SET
|
||||
# Convert 0-100 to 0.0-1.0
|
||||
data[ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100.0
|
||||
|
||||
if entity.domain in config.off_maps_to_on_domains:
|
||||
# Map the off command to on
|
||||
@ -187,9 +205,14 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
# as the actual requested command.
|
||||
config.cached_states[entity_id] = (result, brightness)
|
||||
|
||||
# Perform the requested action
|
||||
yield from hass.services.async_call(core.DOMAIN, service, data,
|
||||
blocking=True)
|
||||
# Separate call to turn on needed
|
||||
if domain != core.DOMAIN:
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True))
|
||||
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
domain, service, data, blocking=True))
|
||||
|
||||
json_response = \
|
||||
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
|
||||
@ -219,23 +242,23 @@ def parse_hue_api_put_light_body(request_json, entity):
|
||||
result = False
|
||||
|
||||
if HUE_API_STATE_BRI in request_json:
|
||||
try:
|
||||
# Clamp brightness from 0 to 255
|
||||
brightness = \
|
||||
max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
try:
|
||||
# Clamp brightness from 0 to 255
|
||||
brightness = \
|
||||
max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
report_brightness = True
|
||||
result = (brightness > 0)
|
||||
elif entity.domain.lower() == "script":
|
||||
# Convert 0-255 to 0-100
|
||||
level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100
|
||||
|
||||
elif entity.domain == "script" or entity.domain == "media_player":
|
||||
# Convert 0-255 to 0-100
|
||||
level = brightness / 255 * 100
|
||||
brightness = round(level)
|
||||
report_brightness = True
|
||||
result = True
|
||||
@ -243,14 +266,34 @@ def parse_hue_api_put_light_body(request_json, entity):
|
||||
return (result, brightness) if report_brightness else (result, None)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if cached_state is None:
|
||||
final_state = entity.state != STATE_OFF
|
||||
final_brightness = entity.attributes.get(
|
||||
ATTR_BRIGHTNESS, 255 if final_state else 0)
|
||||
|
||||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
pass
|
||||
|
||||
elif entity.domain == "media_player":
|
||||
level = entity.attributes.get(
|
||||
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0)
|
||||
# Convert 0.0-1.0 to 0-255
|
||||
final_brightness = round(min(1.0, level) * 255)
|
||||
else:
|
||||
final_state, final_brightness = cached_state
|
||||
|
||||
return (final_state, final_brightness)
|
||||
|
||||
|
||||
def entity_to_json(entity, is_on=None, brightness=None):
|
||||
"""Convert an entity to its Hue bridge JSON representation."""
|
||||
if is_on is None:
|
||||
is_on = entity.state == STATE_ON
|
||||
|
||||
if brightness is None:
|
||||
brightness = 255 if is_on else 0
|
||||
|
||||
name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
|
||||
|
||||
return {
|
||||
|
@ -7,7 +7,9 @@ import requests
|
||||
|
||||
from homeassistant import bootstrap, const, core
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import emulated_hue, http, light, script
|
||||
from homeassistant.components import (
|
||||
emulated_hue, http, light, script, media_player
|
||||
)
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.emulated_hue.hue_api import (
|
||||
HUE_API_STATE_ON, HUE_API_STATE_BRI)
|
||||
@ -73,6 +75,14 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
||||
}
|
||||
})
|
||||
|
||||
bootstrap.setup_component(cls.hass, media_player.DOMAIN, {
|
||||
'media_player': [
|
||||
{
|
||||
'platform': 'demo',
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
cls.hass.start()
|
||||
|
||||
# Kitchen light is explicitly excluded from being exposed
|
||||
@ -111,6 +121,10 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
||||
self.assertTrue('light.bed_light' in result_json)
|
||||
self.assertTrue('script.set_kitchen_light' in result_json)
|
||||
self.assertTrue('light.kitchen_lights' not in result_json)
|
||||
self.assertTrue('media_player.living_room' in result_json)
|
||||
self.assertTrue('media_player.bedroom' in result_json)
|
||||
self.assertTrue('media_player.walkman' in result_json)
|
||||
self.assertTrue('media_player.lounge_room' in result_json)
|
||||
|
||||
def test_get_light_state(self):
|
||||
"""Test the getting of light state."""
|
||||
@ -128,6 +142,21 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
||||
self.assertEqual(office_json['state'][HUE_API_STATE_ON], True)
|
||||
self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127)
|
||||
|
||||
# Check all lights view
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertTrue('application/json' in result.headers['content-type'])
|
||||
|
||||
result_json = result.json()
|
||||
|
||||
self.assertTrue('light.ceiling_lights' in result_json)
|
||||
self.assertEqual(
|
||||
result_json['light.ceiling_lights']['state'][HUE_API_STATE_BRI],
|
||||
127,
|
||||
)
|
||||
|
||||
# Turn bedroom light off
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_OFF,
|
||||
@ -204,15 +233,38 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
||||
self.assertEqual(script_result.status_code, 200)
|
||||
self.assertEqual(len(script_result_json), 2)
|
||||
|
||||
# Wait until script is complete before continuing
|
||||
self.hass.block_till_done()
|
||||
|
||||
kitchen_light = self.hass.states.get('light.kitchen_lights')
|
||||
self.assertEqual(kitchen_light.state, 'on')
|
||||
self.assertEqual(
|
||||
kitchen_light.attributes[light.ATTR_BRIGHTNESS],
|
||||
level)
|
||||
|
||||
def test_put_light_state_media_player(self):
|
||||
"""Test turning on media player and setting volume."""
|
||||
# Turn the music player off first
|
||||
self.hass.services.call(
|
||||
media_player.DOMAIN, const.SERVICE_TURN_OFF,
|
||||
{const.ATTR_ENTITY_ID: 'media_player.walkman'},
|
||||
blocking=True)
|
||||
|
||||
# Emulated hue converts 0.0-1.0 to 0-255.
|
||||
level = 0.25
|
||||
brightness = round(level * 255)
|
||||
|
||||
mp_result = self.perform_put_light_state(
|
||||
'media_player.walkman', True, brightness)
|
||||
|
||||
mp_result_json = mp_result.json()
|
||||
|
||||
self.assertEqual(mp_result.status_code, 200)
|
||||
self.assertEqual(len(mp_result_json), 2)
|
||||
|
||||
walkman = self.hass.states.get('media_player.walkman')
|
||||
self.assertEqual(walkman.state, 'playing')
|
||||
self.assertEqual(
|
||||
walkman.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL],
|
||||
level)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def test_put_with_form_urlencoded_content_type(self):
|
||||
"""Test the form with urlencoded content."""
|
||||
@ -352,4 +404,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
||||
result = requests.put(
|
||||
url, data=json.dumps(data), timeout=5, headers=req_headers)
|
||||
|
||||
# Wait until state change is complete before continuing
|
||||
self.hass.block_till_done()
|
||||
|
||||
return result
|
||||
|
Loading…
x
Reference in New Issue
Block a user