Volume trait for google assistant (#23237)
* Add action.devices.traits.Volume * Drop media player from brightness trait * Factor out commands into separate functions * Drop support for explicit mute
This commit is contained in:
parent
2863ac1068
commit
e11e6e1b04
4 changed files with 145 additions and 51 deletions
|
@ -60,6 +60,7 @@ TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock'
|
|||
TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed'
|
||||
TRAIT_MODES = PREFIX_TRAITS + 'Modes'
|
||||
TRAIT_OPENCLOSE = PREFIX_TRAITS + 'OpenClose'
|
||||
TRAIT_VOLUME = PREFIX_TRAITS + 'Volume'
|
||||
|
||||
PREFIX_COMMANDS = 'action.devices.commands.'
|
||||
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
|
||||
|
@ -79,6 +80,8 @@ COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock'
|
|||
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'
|
||||
COMMAND_MODES = PREFIX_COMMANDS + 'SetModes'
|
||||
COMMAND_OPENCLOSE = PREFIX_COMMANDS + 'OpenClose'
|
||||
COMMAND_SET_VOLUME = PREFIX_COMMANDS + 'setVolume'
|
||||
COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + 'volumeRelative'
|
||||
|
||||
TRAITS = []
|
||||
|
||||
|
@ -141,8 +144,6 @@ class BrightnessTrait(_Trait):
|
|||
"""Test if state is supported."""
|
||||
if domain == light.DOMAIN:
|
||||
return features & light.SUPPORT_BRIGHTNESS
|
||||
if domain == media_player.DOMAIN:
|
||||
return features & media_player.SUPPORT_VOLUME_SET
|
||||
|
||||
return False
|
||||
|
||||
|
@ -160,13 +161,6 @@ class BrightnessTrait(_Trait):
|
|||
if brightness is not None:
|
||||
response['brightness'] = int(100 * (brightness / 255))
|
||||
|
||||
elif domain == media_player.DOMAIN:
|
||||
level = self.state.attributes.get(
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
||||
if level is not None:
|
||||
# Convert 0.0-1.0 to 0-255
|
||||
response['brightness'] = int(level * 100)
|
||||
|
||||
return response
|
||||
|
||||
async def execute(self, command, data, params, challenge):
|
||||
|
@ -179,13 +173,6 @@ class BrightnessTrait(_Trait):
|
|||
ATTR_ENTITY_ID: self.state.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: params['brightness']
|
||||
}, blocking=True, context=data.context)
|
||||
elif domain == media_player.DOMAIN:
|
||||
await self.hass.services.async_call(
|
||||
media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, {
|
||||
ATTR_ENTITY_ID: self.state.entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL:
|
||||
params['brightness'] / 100
|
||||
}, blocking=True, context=data.context)
|
||||
|
||||
|
||||
@register_trait
|
||||
|
@ -1132,6 +1119,81 @@ class OpenCloseTrait(_Trait):
|
|||
'Setting a position is not supported')
|
||||
|
||||
|
||||
@register_trait
|
||||
class VolumeTrait(_Trait):
|
||||
"""Trait to control brightness of a device.
|
||||
|
||||
https://developers.google.com/actions/smarthome/traits/volume
|
||||
"""
|
||||
|
||||
name = TRAIT_VOLUME
|
||||
commands = [
|
||||
COMMAND_SET_VOLUME,
|
||||
COMMAND_VOLUME_RELATIVE,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def supported(domain, features, device_class):
|
||||
"""Test if state is supported."""
|
||||
if domain == media_player.DOMAIN:
|
||||
return features & media_player.SUPPORT_VOLUME_SET
|
||||
|
||||
return False
|
||||
|
||||
def sync_attributes(self):
|
||||
"""Return brightness attributes for a sync request."""
|
||||
return {}
|
||||
|
||||
def query_attributes(self):
|
||||
"""Return brightness query attributes."""
|
||||
response = {}
|
||||
|
||||
level = self.state.attributes.get(
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
||||
muted = self.state.attributes.get(
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED)
|
||||
if level is not None:
|
||||
# Convert 0.0-1.0 to 0-100
|
||||
response['currentVolume'] = int(level * 100)
|
||||
response['isMuted'] = bool(muted)
|
||||
|
||||
return response
|
||||
|
||||
async def _execute_set_volume(self, data, params):
|
||||
level = params['volumeLevel']
|
||||
|
||||
await self.hass.services.async_call(
|
||||
media_player.DOMAIN,
|
||||
media_player.SERVICE_VOLUME_SET, {
|
||||
ATTR_ENTITY_ID: self.state.entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL:
|
||||
level / 100
|
||||
}, blocking=True, context=data.context)
|
||||
|
||||
async def _execute_volume_relative(self, data, params):
|
||||
# This could also support up/down commands using relativeSteps
|
||||
relative = params['volumeRelativeLevel']
|
||||
current = self.state.attributes.get(
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
||||
|
||||
await self.hass.services.async_call(
|
||||
media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, {
|
||||
ATTR_ENTITY_ID: self.state.entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL:
|
||||
current + relative / 100
|
||||
}, blocking=True, context=data.context)
|
||||
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute a brightness command."""
|
||||
if command == COMMAND_SET_VOLUME:
|
||||
await self._execute_set_volume(data, params)
|
||||
elif command == COMMAND_VOLUME_RELATIVE:
|
||||
await self._execute_volume_relative(data, params)
|
||||
else:
|
||||
raise SmartHomeError(
|
||||
ERR_NOT_SUPPORTED, 'Command not supported')
|
||||
|
||||
|
||||
def _verify_pin_challenge(data, challenge):
|
||||
"""Verify a pin challenge."""
|
||||
if not data.config.secure_devices_pin:
|
||||
|
|
|
@ -143,7 +143,7 @@ DEMO_DEVICES = [{
|
|||
},
|
||||
'traits':
|
||||
[
|
||||
'action.devices.traits.OnOff', 'action.devices.traits.Brightness',
|
||||
'action.devices.traits.OnOff', 'action.devices.traits.Volume',
|
||||
'action.devices.traits.Modes'
|
||||
],
|
||||
'type':
|
||||
|
@ -158,7 +158,7 @@ DEMO_DEVICES = [{
|
|||
},
|
||||
'traits':
|
||||
[
|
||||
'action.devices.traits.OnOff', 'action.devices.traits.Brightness',
|
||||
'action.devices.traits.OnOff', 'action.devices.traits.Volume',
|
||||
'action.devices.traits.Modes'
|
||||
],
|
||||
'type':
|
||||
|
@ -180,7 +180,7 @@ DEMO_DEVICES = [{
|
|||
'name': 'Walkman'
|
||||
},
|
||||
'traits':
|
||||
['action.devices.traits.OnOff', 'action.devices.traits.Brightness'],
|
||||
['action.devices.traits.OnOff', 'action.devices.traits.Volume'],
|
||||
'type':
|
||||
'action.devices.types.SWITCH',
|
||||
'willReportState':
|
||||
|
|
|
@ -319,9 +319,9 @@ def test_execute_request(hass_fixture, assistant_client, auth_header):
|
|||
}],
|
||||
"execution": [{
|
||||
"command":
|
||||
"action.devices.commands.BrightnessAbsolute",
|
||||
"action.devices.commands.setVolume",
|
||||
"params": {
|
||||
"brightness": 70
|
||||
"volumeLevel": 70
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
|
|
|
@ -92,36 +92,6 @@ async def test_brightness_light(hass):
|
|||
}
|
||||
|
||||
|
||||
async def test_brightness_media_player(hass):
|
||||
"""Test brightness trait support for media player domain."""
|
||||
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
|
||||
assert trait.BrightnessTrait.supported(media_player.DOMAIN,
|
||||
media_player.SUPPORT_VOLUME_SET,
|
||||
None)
|
||||
|
||||
trt = trait.BrightnessTrait(hass, State(
|
||||
'media_player.bla', media_player.STATE_PLAYING, {
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: .3
|
||||
}), BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
assert trt.query_attributes() == {
|
||||
'brightness': 30
|
||||
}
|
||||
|
||||
calls = async_mock_service(
|
||||
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET)
|
||||
await trt.execute(
|
||||
trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA,
|
||||
{'brightness': 60}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'media_player.bla',
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: .6
|
||||
}
|
||||
|
||||
|
||||
async def test_camera_stream(hass):
|
||||
"""Test camera stream trait support for camera domain."""
|
||||
hass.config.api = Mock(base_url='http://1.1.1.1:8123')
|
||||
|
@ -1276,3 +1246,65 @@ async def test_openclose_binary_sensor(hass, device_class):
|
|||
assert trt.query_attributes() == {
|
||||
'openPercent': 0
|
||||
}
|
||||
|
||||
|
||||
async def test_volume_media_player(hass):
|
||||
"""Test volume trait support for media player domain."""
|
||||
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
|
||||
assert trait.VolumeTrait.supported(media_player.DOMAIN,
|
||||
media_player.SUPPORT_VOLUME_SET |
|
||||
media_player.SUPPORT_VOLUME_MUTE,
|
||||
None)
|
||||
|
||||
trt = trait.VolumeTrait(hass, State(
|
||||
'media_player.bla', media_player.STATE_PLAYING, {
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: .3,
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: False,
|
||||
}), BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
assert trt.query_attributes() == {
|
||||
'currentVolume': 30,
|
||||
'isMuted': False
|
||||
}
|
||||
|
||||
calls = async_mock_service(
|
||||
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET)
|
||||
await trt.execute(
|
||||
trait.COMMAND_SET_VOLUME, BASIC_DATA,
|
||||
{'volumeLevel': 60}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'media_player.bla',
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: .6
|
||||
}
|
||||
|
||||
|
||||
async def test_volume_media_player_relative(hass):
|
||||
"""Test volume trait support for media player domain."""
|
||||
trt = trait.VolumeTrait(hass, State(
|
||||
'media_player.bla', media_player.STATE_PLAYING, {
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: .3,
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: False,
|
||||
}), BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
assert trt.query_attributes() == {
|
||||
'currentVolume': 30,
|
||||
'isMuted': False
|
||||
}
|
||||
|
||||
calls = async_mock_service(
|
||||
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET)
|
||||
|
||||
await trt.execute(
|
||||
trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA,
|
||||
{'volumeRelativeLevel': 20,
|
||||
'relativeSteps': 2}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'media_player.bla',
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: .5
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue