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_FANSPEED = PREFIX_TRAITS + 'FanSpeed'
|
||||||
TRAIT_MODES = PREFIX_TRAITS + 'Modes'
|
TRAIT_MODES = PREFIX_TRAITS + 'Modes'
|
||||||
TRAIT_OPENCLOSE = PREFIX_TRAITS + 'OpenClose'
|
TRAIT_OPENCLOSE = PREFIX_TRAITS + 'OpenClose'
|
||||||
|
TRAIT_VOLUME = PREFIX_TRAITS + 'Volume'
|
||||||
|
|
||||||
PREFIX_COMMANDS = 'action.devices.commands.'
|
PREFIX_COMMANDS = 'action.devices.commands.'
|
||||||
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
|
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
|
||||||
|
@ -79,6 +80,8 @@ COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock'
|
||||||
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'
|
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'
|
||||||
COMMAND_MODES = PREFIX_COMMANDS + 'SetModes'
|
COMMAND_MODES = PREFIX_COMMANDS + 'SetModes'
|
||||||
COMMAND_OPENCLOSE = PREFIX_COMMANDS + 'OpenClose'
|
COMMAND_OPENCLOSE = PREFIX_COMMANDS + 'OpenClose'
|
||||||
|
COMMAND_SET_VOLUME = PREFIX_COMMANDS + 'setVolume'
|
||||||
|
COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + 'volumeRelative'
|
||||||
|
|
||||||
TRAITS = []
|
TRAITS = []
|
||||||
|
|
||||||
|
@ -141,8 +144,6 @@ class BrightnessTrait(_Trait):
|
||||||
"""Test if state is supported."""
|
"""Test if state is supported."""
|
||||||
if domain == light.DOMAIN:
|
if domain == light.DOMAIN:
|
||||||
return features & light.SUPPORT_BRIGHTNESS
|
return features & light.SUPPORT_BRIGHTNESS
|
||||||
if domain == media_player.DOMAIN:
|
|
||||||
return features & media_player.SUPPORT_VOLUME_SET
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -160,13 +161,6 @@ class BrightnessTrait(_Trait):
|
||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
response['brightness'] = int(100 * (brightness / 255))
|
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
|
return response
|
||||||
|
|
||||||
async def execute(self, command, data, params, challenge):
|
async def execute(self, command, data, params, challenge):
|
||||||
|
@ -179,13 +173,6 @@ class BrightnessTrait(_Trait):
|
||||||
ATTR_ENTITY_ID: self.state.entity_id,
|
ATTR_ENTITY_ID: self.state.entity_id,
|
||||||
light.ATTR_BRIGHTNESS_PCT: params['brightness']
|
light.ATTR_BRIGHTNESS_PCT: params['brightness']
|
||||||
}, blocking=True, context=data.context)
|
}, 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
|
@register_trait
|
||||||
|
@ -1132,6 +1119,81 @@ class OpenCloseTrait(_Trait):
|
||||||
'Setting a position is not supported')
|
'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):
|
def _verify_pin_challenge(data, challenge):
|
||||||
"""Verify a pin challenge."""
|
"""Verify a pin challenge."""
|
||||||
if not data.config.secure_devices_pin:
|
if not data.config.secure_devices_pin:
|
||||||
|
|
|
@ -143,7 +143,7 @@ DEMO_DEVICES = [{
|
||||||
},
|
},
|
||||||
'traits':
|
'traits':
|
||||||
[
|
[
|
||||||
'action.devices.traits.OnOff', 'action.devices.traits.Brightness',
|
'action.devices.traits.OnOff', 'action.devices.traits.Volume',
|
||||||
'action.devices.traits.Modes'
|
'action.devices.traits.Modes'
|
||||||
],
|
],
|
||||||
'type':
|
'type':
|
||||||
|
@ -158,7 +158,7 @@ DEMO_DEVICES = [{
|
||||||
},
|
},
|
||||||
'traits':
|
'traits':
|
||||||
[
|
[
|
||||||
'action.devices.traits.OnOff', 'action.devices.traits.Brightness',
|
'action.devices.traits.OnOff', 'action.devices.traits.Volume',
|
||||||
'action.devices.traits.Modes'
|
'action.devices.traits.Modes'
|
||||||
],
|
],
|
||||||
'type':
|
'type':
|
||||||
|
@ -180,7 +180,7 @@ DEMO_DEVICES = [{
|
||||||
'name': 'Walkman'
|
'name': 'Walkman'
|
||||||
},
|
},
|
||||||
'traits':
|
'traits':
|
||||||
['action.devices.traits.OnOff', 'action.devices.traits.Brightness'],
|
['action.devices.traits.OnOff', 'action.devices.traits.Volume'],
|
||||||
'type':
|
'type':
|
||||||
'action.devices.types.SWITCH',
|
'action.devices.types.SWITCH',
|
||||||
'willReportState':
|
'willReportState':
|
||||||
|
|
|
@ -319,9 +319,9 @@ def test_execute_request(hass_fixture, assistant_client, auth_header):
|
||||||
}],
|
}],
|
||||||
"execution": [{
|
"execution": [{
|
||||||
"command":
|
"command":
|
||||||
"action.devices.commands.BrightnessAbsolute",
|
"action.devices.commands.setVolume",
|
||||||
"params": {
|
"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):
|
async def test_camera_stream(hass):
|
||||||
"""Test camera stream trait support for camera domain."""
|
"""Test camera stream trait support for camera domain."""
|
||||||
hass.config.api = Mock(base_url='http://1.1.1.1:8123')
|
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() == {
|
assert trt.query_attributes() == {
|
||||||
'openPercent': 0
|
'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