mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 08:47:10 +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 import core
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON,
|
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET,
|
||||||
STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||||
)
|
)
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
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
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -65,8 +69,11 @@ class HueAllLightsStateView(HomeAssistantView):
|
|||||||
|
|
||||||
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, brightness = 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(entity)
|
json_response[number] = entity_to_json(
|
||||||
|
entity, state, brightness)
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
@ -97,16 +104,9 @@ class HueOneLightStateView(HomeAssistantView):
|
|||||||
_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 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:
|
json_response = entity_to_json(entity, state, brightness)
|
||||||
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)
|
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
@ -158,14 +158,24 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
|
|
||||||
result, brightness = parsed
|
result, brightness = parsed
|
||||||
|
|
||||||
|
# Choose general HA domain
|
||||||
|
domain = core.DOMAIN
|
||||||
|
|
||||||
# Convert the resulting "on" status into the service we need to call
|
# Convert the resulting "on" status into the service we need to call
|
||||||
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
|
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
|
||||||
|
|
||||||
# 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
|
||||||
|
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 the requested entity is a script add some variables
|
||||||
if entity.domain == "script":
|
elif entity.domain == "script":
|
||||||
data['variables'] = {
|
data['variables'] = {
|
||||||
'requested_state': STATE_ON if result else STATE_OFF
|
'requested_state': STATE_ON if result else STATE_OFF
|
||||||
}
|
}
|
||||||
@ -173,8 +183,16 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
data['variables']['requested_level'] = brightness
|
data['variables']['requested_level'] = brightness
|
||||||
|
|
||||||
elif brightness is not None:
|
# If the requested entity is a media player, convert to volume
|
||||||
data[ATTR_BRIGHTNESS] = brightness
|
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:
|
if entity.domain in config.off_maps_to_on_domains:
|
||||||
# Map the off command to on
|
# Map the off command to on
|
||||||
@ -187,9 +205,14 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
# as the actual requested command.
|
# as the actual requested command.
|
||||||
config.cached_states[entity_id] = (result, brightness)
|
config.cached_states[entity_id] = (result, brightness)
|
||||||
|
|
||||||
# Perform the requested action
|
# Separate call to turn on needed
|
||||||
yield from hass.services.async_call(core.DOMAIN, service, data,
|
if domain != core.DOMAIN:
|
||||||
blocking=True)
|
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 = \
|
json_response = \
|
||||||
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
|
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
|
||||||
@ -219,10 +242,6 @@ def parse_hue_api_put_light_body(request_json, entity):
|
|||||||
result = False
|
result = False
|
||||||
|
|
||||||
if HUE_API_STATE_BRI in request_json:
|
if HUE_API_STATE_BRI in request_json:
|
||||||
# Make sure the entity actually supports brightness
|
|
||||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
||||||
|
|
||||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
|
||||||
try:
|
try:
|
||||||
# Clamp brightness from 0 to 255
|
# Clamp brightness from 0 to 255
|
||||||
brightness = \
|
brightness = \
|
||||||
@ -230,12 +249,16 @@ def parse_hue_api_put_light_body(request_json, entity):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
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:
|
||||||
report_brightness = True
|
report_brightness = True
|
||||||
result = (brightness > 0)
|
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)
|
brightness = round(level)
|
||||||
report_brightness = True
|
report_brightness = True
|
||||||
result = 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)
|
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):
|
def entity_to_json(entity, is_on=None, brightness=None):
|
||||||
"""Convert an entity to its Hue bridge JSON representation."""
|
"""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)
|
name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -7,7 +7,9 @@ import requests
|
|||||||
|
|
||||||
from homeassistant import bootstrap, const, core
|
from homeassistant import bootstrap, const, core
|
||||||
import homeassistant.components as core_components
|
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.const import STATE_ON, STATE_OFF
|
||||||
from homeassistant.components.emulated_hue.hue_api import (
|
from homeassistant.components.emulated_hue.hue_api import (
|
||||||
HUE_API_STATE_ON, HUE_API_STATE_BRI)
|
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()
|
cls.hass.start()
|
||||||
|
|
||||||
# Kitchen light is explicitly excluded from being exposed
|
# 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('light.bed_light' in result_json)
|
||||||
self.assertTrue('script.set_kitchen_light' in result_json)
|
self.assertTrue('script.set_kitchen_light' in result_json)
|
||||||
self.assertTrue('light.kitchen_lights' not 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):
|
def test_get_light_state(self):
|
||||||
"""Test the getting of light state."""
|
"""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_ON], True)
|
||||||
self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127)
|
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
|
# Turn bedroom light off
|
||||||
self.hass.services.call(
|
self.hass.services.call(
|
||||||
light.DOMAIN, const.SERVICE_TURN_OFF,
|
light.DOMAIN, const.SERVICE_TURN_OFF,
|
||||||
@ -204,15 +233,38 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||||||
self.assertEqual(script_result.status_code, 200)
|
self.assertEqual(script_result.status_code, 200)
|
||||||
self.assertEqual(len(script_result_json), 2)
|
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')
|
kitchen_light = self.hass.states.get('light.kitchen_lights')
|
||||||
self.assertEqual(kitchen_light.state, 'on')
|
self.assertEqual(kitchen_light.state, 'on')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
kitchen_light.attributes[light.ATTR_BRIGHTNESS],
|
kitchen_light.attributes[light.ATTR_BRIGHTNESS],
|
||||||
level)
|
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
|
# pylint: disable=invalid-name
|
||||||
def test_put_with_form_urlencoded_content_type(self):
|
def test_put_with_form_urlencoded_content_type(self):
|
||||||
"""Test the form with urlencoded content."""
|
"""Test the form with urlencoded content."""
|
||||||
@ -352,4 +404,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
|||||||
result = requests.put(
|
result = requests.put(
|
||||||
url, data=json.dumps(data), timeout=5, headers=req_headers)
|
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
|
return result
|
||||||
|
Loading…
x
Reference in New Issue
Block a user