Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component This is supported by the smarthome API, but there is no documentation for it. This work is based on an article I found with screenshots of documentation that was erroneously uploaded: https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/ Google Assistant now supports unlocking certain locks - Nest and August come to mind - via this API, and this commit allows Home Assistant to do so as well. Notably, I've added a config option `allow_unlock` that controls whether we actually honor requests to unlock a lock via the google assistant. It defaults to false. Additionally, we add the functionNotSupported error, which makes a little more sense when we're unable to execute the desired state transition. https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list * Fix linter warnings * Ensure that certain groups are never exposed to cloud entities For example, the group.all_locks entity - we should probably never expose this to third party cloud integrations. It's risky. This is not configurable, but can be extended by adding to the cloud.const.NEVER_EXPOSED_ENTITIES array. It's implemented in a modestly hacky fashion, because we determine whether or not a entity should be excluded/included in several ways. Notably, we define this array in the top level const.py, to avoid circular import problems between the cloud/alexa components.
This commit is contained in:
parent
ddee5f8b86
commit
2bf2214d51
14 changed files with 283 additions and 43 deletions
|
@ -11,6 +11,7 @@ from homeassistant.components import (
|
|||
fan,
|
||||
input_boolean,
|
||||
light,
|
||||
lock,
|
||||
media_player,
|
||||
scene,
|
||||
script,
|
||||
|
@ -23,6 +24,17 @@ from homeassistant.util import color
|
|||
|
||||
from tests.common import async_mock_service
|
||||
|
||||
BASIC_CONFIG = helpers.Config(
|
||||
should_expose=lambda state: True,
|
||||
agent_user_id='test-agent',
|
||||
)
|
||||
|
||||
UNSAFE_CONFIG = helpers.Config(
|
||||
should_expose=lambda state: True,
|
||||
agent_user_id='test-agent',
|
||||
allow_unlock=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_brightness_light(hass):
|
||||
"""Test brightness trait support for light domain."""
|
||||
|
@ -31,7 +43,7 @@ async def test_brightness_light(hass):
|
|||
|
||||
trt = trait.BrightnessTrait(hass, State('light.bla', light.STATE_ON, {
|
||||
light.ATTR_BRIGHTNESS: 243
|
||||
}))
|
||||
}), BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
|
@ -57,7 +69,7 @@ async def test_brightness_cover(hass):
|
|||
|
||||
trt = trait.BrightnessTrait(hass, State('cover.bla', cover.STATE_OPEN, {
|
||||
cover.ATTR_CURRENT_POSITION: 75
|
||||
}))
|
||||
}), BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
|
@ -85,7 +97,7 @@ async def test_brightness_media_player(hass):
|
|||
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() == {}
|
||||
|
||||
|
@ -109,7 +121,7 @@ async def test_onoff_group(hass):
|
|||
"""Test OnOff trait support for group domain."""
|
||||
assert trait.OnOffTrait.supported(group.DOMAIN, 0)
|
||||
|
||||
trt_on = trait.OnOffTrait(hass, State('group.bla', STATE_ON))
|
||||
trt_on = trait.OnOffTrait(hass, State('group.bla', STATE_ON), BASIC_CONFIG)
|
||||
|
||||
assert trt_on.sync_attributes() == {}
|
||||
|
||||
|
@ -117,7 +129,9 @@ async def test_onoff_group(hass):
|
|||
'on': True
|
||||
}
|
||||
|
||||
trt_off = trait.OnOffTrait(hass, State('group.bla', STATE_OFF))
|
||||
trt_off = trait.OnOffTrait(hass, State('group.bla', STATE_OFF),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt_off.query_attributes() == {
|
||||
'on': False
|
||||
}
|
||||
|
@ -145,7 +159,8 @@ async def test_onoff_input_boolean(hass):
|
|||
"""Test OnOff trait support for input_boolean domain."""
|
||||
assert trait.OnOffTrait.supported(input_boolean.DOMAIN, 0)
|
||||
|
||||
trt_on = trait.OnOffTrait(hass, State('input_boolean.bla', STATE_ON))
|
||||
trt_on = trait.OnOffTrait(hass, State('input_boolean.bla', STATE_ON),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt_on.sync_attributes() == {}
|
||||
|
||||
|
@ -153,7 +168,9 @@ async def test_onoff_input_boolean(hass):
|
|||
'on': True
|
||||
}
|
||||
|
||||
trt_off = trait.OnOffTrait(hass, State('input_boolean.bla', STATE_OFF))
|
||||
trt_off = trait.OnOffTrait(hass, State('input_boolean.bla', STATE_OFF),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt_off.query_attributes() == {
|
||||
'on': False
|
||||
}
|
||||
|
@ -182,7 +199,8 @@ async def test_onoff_switch(hass):
|
|||
"""Test OnOff trait support for switch domain."""
|
||||
assert trait.OnOffTrait.supported(switch.DOMAIN, 0)
|
||||
|
||||
trt_on = trait.OnOffTrait(hass, State('switch.bla', STATE_ON))
|
||||
trt_on = trait.OnOffTrait(hass, State('switch.bla', STATE_ON),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt_on.sync_attributes() == {}
|
||||
|
||||
|
@ -190,7 +208,9 @@ async def test_onoff_switch(hass):
|
|||
'on': True
|
||||
}
|
||||
|
||||
trt_off = trait.OnOffTrait(hass, State('switch.bla', STATE_OFF))
|
||||
trt_off = trait.OnOffTrait(hass, State('switch.bla', STATE_OFF),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt_off.query_attributes() == {
|
||||
'on': False
|
||||
}
|
||||
|
@ -218,7 +238,7 @@ async def test_onoff_fan(hass):
|
|||
"""Test OnOff trait support for fan domain."""
|
||||
assert trait.OnOffTrait.supported(fan.DOMAIN, 0)
|
||||
|
||||
trt_on = trait.OnOffTrait(hass, State('fan.bla', STATE_ON))
|
||||
trt_on = trait.OnOffTrait(hass, State('fan.bla', STATE_ON), BASIC_CONFIG)
|
||||
|
||||
assert trt_on.sync_attributes() == {}
|
||||
|
||||
|
@ -226,7 +246,7 @@ async def test_onoff_fan(hass):
|
|||
'on': True
|
||||
}
|
||||
|
||||
trt_off = trait.OnOffTrait(hass, State('fan.bla', STATE_OFF))
|
||||
trt_off = trait.OnOffTrait(hass, State('fan.bla', STATE_OFF), BASIC_CONFIG)
|
||||
assert trt_off.query_attributes() == {
|
||||
'on': False
|
||||
}
|
||||
|
@ -254,7 +274,7 @@ async def test_onoff_light(hass):
|
|||
"""Test OnOff trait support for light domain."""
|
||||
assert trait.OnOffTrait.supported(light.DOMAIN, 0)
|
||||
|
||||
trt_on = trait.OnOffTrait(hass, State('light.bla', STATE_ON))
|
||||
trt_on = trait.OnOffTrait(hass, State('light.bla', STATE_ON), BASIC_CONFIG)
|
||||
|
||||
assert trt_on.sync_attributes() == {}
|
||||
|
||||
|
@ -262,7 +282,9 @@ async def test_onoff_light(hass):
|
|||
'on': True
|
||||
}
|
||||
|
||||
trt_off = trait.OnOffTrait(hass, State('light.bla', STATE_OFF))
|
||||
trt_off = trait.OnOffTrait(hass, State('light.bla', STATE_OFF),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt_off.query_attributes() == {
|
||||
'on': False
|
||||
}
|
||||
|
@ -290,7 +312,8 @@ async def test_onoff_cover(hass):
|
|||
"""Test OnOff trait support for cover domain."""
|
||||
assert trait.OnOffTrait.supported(cover.DOMAIN, 0)
|
||||
|
||||
trt_on = trait.OnOffTrait(hass, State('cover.bla', cover.STATE_OPEN))
|
||||
trt_on = trait.OnOffTrait(hass, State('cover.bla', cover.STATE_OPEN),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt_on.sync_attributes() == {}
|
||||
|
||||
|
@ -298,7 +321,9 @@ async def test_onoff_cover(hass):
|
|||
'on': True
|
||||
}
|
||||
|
||||
trt_off = trait.OnOffTrait(hass, State('cover.bla', cover.STATE_CLOSED))
|
||||
trt_off = trait.OnOffTrait(hass, State('cover.bla', cover.STATE_CLOSED),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt_off.query_attributes() == {
|
||||
'on': False
|
||||
}
|
||||
|
@ -327,7 +352,8 @@ async def test_onoff_media_player(hass):
|
|||
"""Test OnOff trait support for media_player domain."""
|
||||
assert trait.OnOffTrait.supported(media_player.DOMAIN, 0)
|
||||
|
||||
trt_on = trait.OnOffTrait(hass, State('media_player.bla', STATE_ON))
|
||||
trt_on = trait.OnOffTrait(hass, State('media_player.bla', STATE_ON),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt_on.sync_attributes() == {}
|
||||
|
||||
|
@ -335,7 +361,9 @@ async def test_onoff_media_player(hass):
|
|||
'on': True
|
||||
}
|
||||
|
||||
trt_off = trait.OnOffTrait(hass, State('media_player.bla', STATE_OFF))
|
||||
trt_off = trait.OnOffTrait(hass, State('media_player.bla', STATE_OFF),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt_off.query_attributes() == {
|
||||
'on': False
|
||||
}
|
||||
|
@ -349,7 +377,9 @@ async def test_onoff_media_player(hass):
|
|||
ATTR_ENTITY_ID: 'media_player.bla',
|
||||
}
|
||||
|
||||
off_calls = async_mock_service(hass, media_player.DOMAIN, SERVICE_TURN_OFF)
|
||||
off_calls = async_mock_service(hass, media_player.DOMAIN,
|
||||
SERVICE_TURN_OFF)
|
||||
|
||||
await trt_on.execute(trait.COMMAND_ONOFF, {
|
||||
'on': False
|
||||
})
|
||||
|
@ -363,7 +393,8 @@ async def test_dock_vacuum(hass):
|
|||
"""Test dock trait support for vacuum domain."""
|
||||
assert trait.DockTrait.supported(vacuum.DOMAIN, 0)
|
||||
|
||||
trt = trait.DockTrait(hass, State('vacuum.bla', vacuum.STATE_IDLE))
|
||||
trt = trait.DockTrait(hass, State('vacuum.bla', vacuum.STATE_IDLE),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
|
@ -386,7 +417,7 @@ async def test_startstop_vacuum(hass):
|
|||
|
||||
trt = trait.StartStopTrait(hass, State('vacuum.bla', vacuum.STATE_PAUSED, {
|
||||
ATTR_SUPPORTED_FEATURES: vacuum.SUPPORT_PAUSE,
|
||||
}))
|
||||
}), BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {'pausable': True}
|
||||
|
||||
|
@ -436,7 +467,7 @@ async def test_color_spectrum_light(hass):
|
|||
|
||||
trt = trait.ColorSpectrumTrait(hass, State('light.bla', STATE_ON, {
|
||||
light.ATTR_HS_COLOR: (0, 94),
|
||||
}))
|
||||
}), BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {
|
||||
'colorModel': 'rgb'
|
||||
|
@ -482,7 +513,7 @@ async def test_color_temperature_light(hass):
|
|||
light.ATTR_MIN_MIREDS: 200,
|
||||
light.ATTR_COLOR_TEMP: 300,
|
||||
light.ATTR_MAX_MIREDS: 500,
|
||||
}))
|
||||
}), BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {
|
||||
'temperatureMinK': 2000,
|
||||
|
@ -538,7 +569,7 @@ async def test_color_temperature_light_bad_temp(hass):
|
|||
light.ATTR_MIN_MIREDS: 200,
|
||||
light.ATTR_COLOR_TEMP: 0,
|
||||
light.ATTR_MAX_MIREDS: 500,
|
||||
}))
|
||||
}), BASIC_CONFIG)
|
||||
|
||||
assert trt.query_attributes() == {
|
||||
}
|
||||
|
@ -548,7 +579,7 @@ async def test_scene_scene(hass):
|
|||
"""Test Scene trait support for scene domain."""
|
||||
assert trait.SceneTrait.supported(scene.DOMAIN, 0)
|
||||
|
||||
trt = trait.SceneTrait(hass, State('scene.bla', scene.STATE))
|
||||
trt = trait.SceneTrait(hass, State('scene.bla', scene.STATE), BASIC_CONFIG)
|
||||
assert trt.sync_attributes() == {}
|
||||
assert trt.query_attributes() == {}
|
||||
assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {})
|
||||
|
@ -565,7 +596,7 @@ async def test_scene_script(hass):
|
|||
"""Test Scene trait support for script domain."""
|
||||
assert trait.SceneTrait.supported(script.DOMAIN, 0)
|
||||
|
||||
trt = trait.SceneTrait(hass, State('script.bla', STATE_OFF))
|
||||
trt = trait.SceneTrait(hass, State('script.bla', STATE_OFF), BASIC_CONFIG)
|
||||
assert trt.sync_attributes() == {}
|
||||
assert trt.query_attributes() == {}
|
||||
assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {})
|
||||
|
@ -605,7 +636,7 @@ async def test_temperature_setting_climate_range(hass):
|
|||
climate.ATTR_TARGET_TEMP_LOW: 65,
|
||||
climate.ATTR_MIN_TEMP: 50,
|
||||
climate.ATTR_MAX_TEMP: 80
|
||||
}))
|
||||
}), BASIC_CONFIG)
|
||||
assert trt.sync_attributes() == {
|
||||
'availableThermostatModes': 'off,cool,heat,heatcool',
|
||||
'thermostatTemperatureUnit': 'F',
|
||||
|
@ -672,7 +703,7 @@ async def test_temperature_setting_climate_setpoint(hass):
|
|||
climate.ATTR_MAX_TEMP: 30,
|
||||
climate.ATTR_TEMPERATURE: 18,
|
||||
climate.ATTR_CURRENT_TEMPERATURE: 20
|
||||
}))
|
||||
}), BASIC_CONFIG)
|
||||
assert trt.sync_attributes() == {
|
||||
'availableThermostatModes': 'off,cool',
|
||||
'thermostatTemperatureUnit': 'C',
|
||||
|
@ -702,3 +733,65 @@ async def test_temperature_setting_climate_setpoint(hass):
|
|||
ATTR_ENTITY_ID: 'climate.bla',
|
||||
climate.ATTR_TEMPERATURE: 19
|
||||
}
|
||||
|
||||
|
||||
async def test_lock_unlock_lock(hass):
|
||||
"""Test LockUnlock trait locking support for lock domain."""
|
||||
assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN)
|
||||
|
||||
trt = trait.LockUnlockTrait(hass,
|
||||
State('lock.front_door', lock.STATE_UNLOCKED),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
assert trt.query_attributes() == {
|
||||
'isLocked': False
|
||||
}
|
||||
|
||||
assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': True})
|
||||
|
||||
calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_LOCK)
|
||||
await trt.execute(trait.COMMAND_LOCKUNLOCK, {'lock': True})
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'lock.front_door'
|
||||
}
|
||||
|
||||
|
||||
async def test_lock_unlock_unlock(hass):
|
||||
"""Test LockUnlock trait unlocking support for lock domain."""
|
||||
assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN)
|
||||
|
||||
trt = trait.LockUnlockTrait(hass,
|
||||
State('lock.front_door', lock.STATE_LOCKED),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
assert trt.query_attributes() == {
|
||||
'isLocked': True
|
||||
}
|
||||
|
||||
assert not trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': False})
|
||||
|
||||
trt = trait.LockUnlockTrait(hass,
|
||||
State('lock.front_door', lock.STATE_LOCKED),
|
||||
UNSAFE_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
assert trt.query_attributes() == {
|
||||
'isLocked': True
|
||||
}
|
||||
|
||||
assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': False})
|
||||
|
||||
calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_UNLOCK)
|
||||
await trt.execute(trait.COMMAND_LOCKUNLOCK, {'lock': False})
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'lock.front_door'
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue