Google assistant skip missing type (#23174)

* Skip entity if no device type found

* Add test for potentially skipped binary sensors

* Reorg code, add tests to ensure all exposed things have types

* Lint

* Fix tests

* Lint
This commit is contained in:
Joakim Plate 2019-04-18 07:37:39 +02:00 committed by Paulus Schoutsen
parent ce8ec3acb1
commit 4a2a130bfa
9 changed files with 285 additions and 240 deletions

View file

@ -14,7 +14,8 @@ from homeassistant.components.http.data_validator import (
RequestDataValidator) RequestDataValidator)
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import smart_home as google_sh from homeassistant.components.google_assistant import (
const as google_const)
from .const import ( from .const import (
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
@ -415,7 +416,7 @@ def _account_data(cloud):
'cloud': cloud.iot.state, 'cloud': cloud.iot.state,
'prefs': client.prefs.as_dict(), 'prefs': client.prefs.as_dict(),
'google_entities': client.google_user_config['filter'].config, 'google_entities': client.google_user_config['filter'].config,
'google_domains': list(google_sh.DOMAIN_TO_GOOGLE_TYPES), 'google_domains': list(google_const.DOMAIN_TO_GOOGLE_TYPES),
'alexa_entities': client.alexa_config.should_expose.config, 'alexa_entities': client.alexa_config.should_expose.config,
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS), 'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain, 'remote_domain': remote.instance_domain,

View file

@ -1,4 +1,20 @@
"""Constants for Google Assistant.""" """Constants for Google Assistant."""
from homeassistant.components import (
binary_sensor,
camera,
climate,
cover,
fan,
group,
input_boolean,
light,
lock,
media_player,
scene,
script,
switch,
vacuum,
)
DOMAIN = 'google_assistant' DOMAIN = 'google_assistant'
GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant' GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant'
@ -32,6 +48,7 @@ TYPE_LOCK = PREFIX_TYPES + 'LOCK'
TYPE_BLINDS = PREFIX_TYPES + 'BLINDS' TYPE_BLINDS = PREFIX_TYPES + 'BLINDS'
TYPE_GARAGE = PREFIX_TYPES + 'GARAGE' TYPE_GARAGE = PREFIX_TYPES + 'GARAGE'
TYPE_OUTLET = PREFIX_TYPES + 'OUTLET' TYPE_OUTLET = PREFIX_TYPES + 'OUTLET'
TYPE_SENSOR = PREFIX_TYPES + 'SENSOR'
SERVICE_REQUEST_SYNC = 'request_sync' SERVICE_REQUEST_SYNC = 'request_sync'
HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' HOMEGRAPH_URL = 'https://homegraph.googleapis.com/'
@ -51,3 +68,32 @@ ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported'
EVENT_COMMAND_RECEIVED = 'google_assistant_command' EVENT_COMMAND_RECEIVED = 'google_assistant_command'
EVENT_QUERY_RECEIVED = 'google_assistant_query' EVENT_QUERY_RECEIVED = 'google_assistant_query'
EVENT_SYNC_RECEIVED = 'google_assistant_sync' EVENT_SYNC_RECEIVED = 'google_assistant_sync'
DOMAIN_TO_GOOGLE_TYPES = {
camera.DOMAIN: TYPE_CAMERA,
climate.DOMAIN: TYPE_THERMOSTAT,
cover.DOMAIN: TYPE_BLINDS,
fan.DOMAIN: TYPE_FAN,
group.DOMAIN: TYPE_SWITCH,
input_boolean.DOMAIN: TYPE_SWITCH,
light.DOMAIN: TYPE_LIGHT,
lock.DOMAIN: TYPE_LOCK,
media_player.DOMAIN: TYPE_SWITCH,
scene.DOMAIN: TYPE_SCENE,
script.DOMAIN: TYPE_SCENE,
switch.DOMAIN: TYPE_SWITCH,
vacuum.DOMAIN: TYPE_VACUUM,
}
DEVICE_CLASS_TO_GOOGLE_TYPES = {
(cover.DOMAIN, cover.DEVICE_CLASS_GARAGE): TYPE_GARAGE,
(switch.DOMAIN, switch.DEVICE_CLASS_SWITCH): TYPE_SWITCH,
(switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET,
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_DOOR): TYPE_SENSOR,
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_GARAGE_DOOR):
TYPE_SENSOR,
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_LOCK): TYPE_SENSOR,
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR,
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR,
}

View file

@ -0,0 +1,13 @@
"""Errors for Google Assistant."""
class SmartHomeError(Exception):
"""Google Assistant Smart Home errors.
https://developers.google.com/actions/smarthome/create-app#error_responses
"""
def __init__(self, code, msg):
"""Log error code."""
super().__init__(msg)
self.code = code

View file

@ -1,17 +1,19 @@
"""Helper classes for Google Assistant integration.""" """Helper classes for Google Assistant integration."""
from homeassistant.core import Context from asyncio import gather
from collections.abc import Mapping
from homeassistant.core import Context, callback
from homeassistant.const import (
CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES,
ATTR_DEVICE_CLASS
)
class SmartHomeError(Exception): from . import trait
"""Google Assistant Smart Home errors. from .const import (
DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED,
https://developers.google.com/actions/smarthome/create-app#error_responses DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT,
""" )
from .error import SmartHomeError
def __init__(self, code, msg):
"""Log error code."""
super().__init__(msg)
self.code = code
class Config: class Config:
@ -33,3 +35,174 @@ class RequestData:
self.config = config self.config = config
self.request_id = request_id self.request_id = request_id
self.context = Context(user_id=user_id) self.context = Context(user_id=user_id)
def get_google_type(domain, device_class):
"""Google type based on domain and device class."""
typ = DEVICE_CLASS_TO_GOOGLE_TYPES.get((domain, device_class))
return typ if typ is not None else DOMAIN_TO_GOOGLE_TYPES[domain]
class GoogleEntity:
"""Adaptation of Entity expressed in Google's terms."""
def __init__(self, hass, config, state):
"""Initialize a Google entity."""
self.hass = hass
self.config = config
self.state = state
self._traits = None
@property
def entity_id(self):
"""Return entity ID."""
return self.state.entity_id
@callback
def traits(self):
"""Return traits for entity."""
if self._traits is not None:
return self._traits
state = self.state
domain = state.domain
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
self._traits = [Trait(self.hass, state, self.config)
for Trait in trait.TRAITS
if Trait.supported(domain, features, device_class)]
return self._traits
async def sync_serialize(self):
"""Serialize entity for a SYNC response.
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
"""
state = self.state
# When a state is unavailable, the attributes that describe
# capabilities will be stripped. For example, a light entity will miss
# the min/max mireds. Therefore they will be excluded from a sync.
if state.state == STATE_UNAVAILABLE:
return None
entity_config = self.config.entity_config.get(state.entity_id, {})
name = (entity_config.get(CONF_NAME) or state.name).strip()
domain = state.domain
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
# If an empty string
if not name:
return None
traits = self.traits()
# Found no supported traits for this entity
if not traits:
return None
device_type = get_google_type(domain,
device_class)
device = {
'id': state.entity_id,
'name': {
'name': name
},
'attributes': {},
'traits': [trait.name for trait in traits],
'willReportState': False,
'type': device_type,
}
# use aliases
aliases = entity_config.get(CONF_ALIASES)
if aliases:
device['name']['nicknames'] = aliases
for trt in traits:
device['attributes'].update(trt.sync_attributes())
room = entity_config.get(CONF_ROOM_HINT)
if room:
device['roomHint'] = room
return device
dev_reg, ent_reg, area_reg = await gather(
self.hass.helpers.device_registry.async_get_registry(),
self.hass.helpers.entity_registry.async_get_registry(),
self.hass.helpers.area_registry.async_get_registry(),
)
entity_entry = ent_reg.async_get(state.entity_id)
if not (entity_entry and entity_entry.device_id):
return device
device_entry = dev_reg.devices.get(entity_entry.device_id)
if not (device_entry and device_entry.area_id):
return device
area_entry = area_reg.areas.get(device_entry.area_id)
if area_entry and area_entry.name:
device['roomHint'] = area_entry.name
return device
@callback
def query_serialize(self):
"""Serialize entity for a QUERY response.
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
"""
state = self.state
if state.state == STATE_UNAVAILABLE:
return {'online': False}
attrs = {'online': True}
for trt in self.traits():
deep_update(attrs, trt.query_attributes())
return attrs
async def execute(self, command, data, params):
"""Execute a command.
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
"""
executed = False
for trt in self.traits():
if trt.can_execute(command, params):
await trt.execute(command, data, params)
executed = True
break
if not executed:
raise SmartHomeError(
ERR_FUNCTION_NOT_SUPPORTED,
'Unable to execute {} for {}'.format(command,
self.state.entity_id))
@callback
def async_update(self):
"""Update the entity with latest info from Home Assistant."""
self.state = self.hass.states.get(self.entity_id)
if self._traits is None:
return
for trt in self._traits:
trt.state = self.state
def deep_update(target, source):
"""Update a nested dictionary with another nested dictionary."""
for key, value in source.items():
if isinstance(value, Mapping):
target[key] = deep_update(target.get(key, {}), value)
else:
target[key] = value
return target

View file

@ -1,237 +1,22 @@
"""Support for Google Assistant Smart Home API.""" """Support for Google Assistant Smart Home API."""
from asyncio import gather
from collections.abc import Mapping
from itertools import product from itertools import product
import logging import logging
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.core import callback
from homeassistant.const import ( from homeassistant.const import (
CLOUD_NEVER_EXPOSED_ENTITIES, CONF_NAME, STATE_UNAVAILABLE, CLOUD_NEVER_EXPOSED_ENTITIES, ATTR_ENTITY_ID)
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ATTR_DEVICE_CLASS,
)
from homeassistant.components import (
camera,
climate,
cover,
fan,
group,
input_boolean,
light,
lock,
media_player,
scene,
script,
switch,
vacuum,
)
from . import trait
from .const import ( from .const import (
TYPE_LIGHT, TYPE_LOCK, TYPE_SCENE, TYPE_SWITCH, TYPE_VACUUM, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR,
TYPE_THERMOSTAT, TYPE_FAN, TYPE_CAMERA, TYPE_BLINDS, TYPE_GARAGE,
TYPE_OUTLET,
CONF_ALIASES, CONF_ROOM_HINT,
ERR_FUNCTION_NOT_SUPPORTED, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE,
ERR_UNKNOWN_ERROR,
EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED
) )
from .helpers import SmartHomeError, RequestData from .helpers import RequestData, GoogleEntity
from .error import SmartHomeError
HANDLERS = Registry() HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN_TO_GOOGLE_TYPES = {
camera.DOMAIN: TYPE_CAMERA,
climate.DOMAIN: TYPE_THERMOSTAT,
cover.DOMAIN: TYPE_BLINDS,
fan.DOMAIN: TYPE_FAN,
group.DOMAIN: TYPE_SWITCH,
input_boolean.DOMAIN: TYPE_SWITCH,
light.DOMAIN: TYPE_LIGHT,
lock.DOMAIN: TYPE_LOCK,
media_player.DOMAIN: TYPE_SWITCH,
scene.DOMAIN: TYPE_SCENE,
script.DOMAIN: TYPE_SCENE,
switch.DOMAIN: TYPE_SWITCH,
vacuum.DOMAIN: TYPE_VACUUM,
}
DEVICE_CLASS_TO_GOOGLE_TYPES = {
(cover.DOMAIN, cover.DEVICE_CLASS_GARAGE): TYPE_GARAGE,
(switch.DOMAIN, switch.DEVICE_CLASS_SWITCH): TYPE_SWITCH,
(switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET,
}
def deep_update(target, source):
"""Update a nested dictionary with another nested dictionary."""
for key, value in source.items():
if isinstance(value, Mapping):
target[key] = deep_update(target.get(key, {}), value)
else:
target[key] = value
return target
def get_google_type(domain, device_class):
"""Google type based on domain and device class."""
typ = DEVICE_CLASS_TO_GOOGLE_TYPES.get((domain, device_class))
return typ if typ is not None else DOMAIN_TO_GOOGLE_TYPES.get(domain)
class _GoogleEntity:
"""Adaptation of Entity expressed in Google's terms."""
def __init__(self, hass, config, state):
self.hass = hass
self.config = config
self.state = state
self._traits = None
@property
def entity_id(self):
"""Return entity ID."""
return self.state.entity_id
@callback
def traits(self):
"""Return traits for entity."""
if self._traits is not None:
return self._traits
state = self.state
domain = state.domain
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
self._traits = [Trait(self.hass, state, self.config)
for Trait in trait.TRAITS
if Trait.supported(domain, features, device_class)]
return self._traits
async def sync_serialize(self):
"""Serialize entity for a SYNC response.
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
"""
state = self.state
# When a state is unavailable, the attributes that describe
# capabilities will be stripped. For example, a light entity will miss
# the min/max mireds. Therefore they will be excluded from a sync.
if state.state == STATE_UNAVAILABLE:
return None
entity_config = self.config.entity_config.get(state.entity_id, {})
name = (entity_config.get(CONF_NAME) or state.name).strip()
domain = state.domain
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
# If an empty string
if not name:
return None
traits = self.traits()
# Found no supported traits for this entity
if not traits:
return None
device = {
'id': state.entity_id,
'name': {
'name': name
},
'attributes': {},
'traits': [trait.name for trait in traits],
'willReportState': False,
'type': get_google_type(domain, device_class),
}
# use aliases
aliases = entity_config.get(CONF_ALIASES)
if aliases:
device['name']['nicknames'] = aliases
for trt in traits:
device['attributes'].update(trt.sync_attributes())
room = entity_config.get(CONF_ROOM_HINT)
if room:
device['roomHint'] = room
return device
dev_reg, ent_reg, area_reg = await gather(
self.hass.helpers.device_registry.async_get_registry(),
self.hass.helpers.entity_registry.async_get_registry(),
self.hass.helpers.area_registry.async_get_registry(),
)
entity_entry = ent_reg.async_get(state.entity_id)
if not (entity_entry and entity_entry.device_id):
return device
device_entry = dev_reg.devices.get(entity_entry.device_id)
if not (device_entry and device_entry.area_id):
return device
area_entry = area_reg.areas.get(device_entry.area_id)
if area_entry and area_entry.name:
device['roomHint'] = area_entry.name
return device
@callback
def query_serialize(self):
"""Serialize entity for a QUERY response.
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
"""
state = self.state
if state.state == STATE_UNAVAILABLE:
return {'online': False}
attrs = {'online': True}
for trt in self.traits():
deep_update(attrs, trt.query_attributes())
return attrs
async def execute(self, command, data, params):
"""Execute a command.
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
"""
executed = False
for trt in self.traits():
if trt.can_execute(command, params):
await trt.execute(command, data, params)
executed = True
break
if not executed:
raise SmartHomeError(
ERR_FUNCTION_NOT_SUPPORTED,
'Unable to execute {} for {}'.format(command,
self.state.entity_id))
@callback
def async_update(self):
"""Update the entity with latest info from Home Assistant."""
self.state = self.hass.states.get(self.entity_id)
if self._traits is None:
return
for trt in self._traits:
trt.state = self.state
async def async_handle_message(hass, config, user_id, message): async def async_handle_message(hass, config, user_id, message):
"""Handle incoming API messages.""" """Handle incoming API messages."""
@ -304,7 +89,7 @@ async def async_devices_sync(hass, data, payload):
if not data.config.should_expose(state): if not data.config.should_expose(state):
continue continue
entity = _GoogleEntity(hass, data.config, state) entity = GoogleEntity(hass, data.config, state)
serialized = await entity.sync_serialize() serialized = await entity.sync_serialize()
if serialized is None: if serialized is None:
@ -345,7 +130,7 @@ async def async_devices_query(hass, data, payload):
devices[devid] = {'online': False} devices[devid] = {'online': False}
continue continue
entity = _GoogleEntity(hass, data.config, state) entity = GoogleEntity(hass, data.config, state)
devices[devid] = entity.query_serialize() devices[devid] = entity.query_serialize()
return {'devices': devices} return {'devices': devices}
@ -389,7 +174,7 @@ async def handle_devices_execute(hass, data, payload):
} }
continue continue
entities[entity_id] = _GoogleEntity(hass, data.config, state) entities[entity_id] = GoogleEntity(hass, data.config, state)
try: try:
await entities[entity_id].execute(execution['command'], await entities[entity_id].execute(execution['command'],

View file

@ -38,7 +38,7 @@ from .const import (
ERR_NOT_SUPPORTED, ERR_NOT_SUPPORTED,
ERR_FUNCTION_NOT_SUPPORTED, ERR_FUNCTION_NOT_SUPPORTED,
) )
from .helpers import SmartHomeError from .error import SmartHomeError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -335,7 +335,7 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture,
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
with patch.dict( with patch.dict(
'homeassistant.components.google_assistant.smart_home.' 'homeassistant.components.google_assistant.const.'
'DOMAIN_TO_GOOGLE_TYPES', {'light': None}, clear=True 'DOMAIN_TO_GOOGLE_TYPES', {'light': None}, clear=True
), patch.dict('homeassistant.components.alexa.smart_home.ENTITY_ADAPTERS', ), patch.dict('homeassistant.components.alexa.smart_home.ENTITY_ADAPTERS',
{'switch': None}, clear=True): {'switch': None}, clear=True):

View file

@ -96,7 +96,7 @@ async def test_sync_message(hass):
trait.TRAIT_ONOFF, trait.TRAIT_ONOFF,
trait.TRAIT_COLOR_SETTING, trait.TRAIT_COLOR_SETTING,
], ],
'type': sh.TYPE_LIGHT, 'type': const.TYPE_LIGHT,
'willReportState': False, 'willReportState': False,
'attributes': { 'attributes': {
'colorModel': 'hsv', 'colorModel': 'hsv',
@ -176,7 +176,7 @@ async def test_sync_in_area(hass, registries):
trait.TRAIT_ONOFF, trait.TRAIT_ONOFF,
trait.TRAIT_COLOR_SETTING, trait.TRAIT_COLOR_SETTING,
], ],
'type': sh.TYPE_LIGHT, 'type': const.TYPE_LIGHT,
'willReportState': False, 'willReportState': False,
'attributes': { 'attributes': {
'colorModel': 'hsv', 'colorModel': 'hsv',
@ -489,7 +489,7 @@ async def test_serialize_input_boolean(hass):
"""Test serializing an input boolean entity.""" """Test serializing an input boolean entity."""
state = State('input_boolean.bla', 'on') state = State('input_boolean.bla', 'on')
# pylint: disable=protected-access # pylint: disable=protected-access
entity = sh._GoogleEntity(hass, BASIC_CONFIG, state) entity = sh.GoogleEntity(hass, BASIC_CONFIG, state)
result = await entity.sync_serialize() result = await entity.sync_serialize()
assert result == { assert result == {
'id': 'input_boolean.bla', 'id': 'input_boolean.bla',

View file

@ -49,6 +49,7 @@ UNSAFE_CONFIG = helpers.Config(
async def test_brightness_light(hass): async def test_brightness_light(hass):
"""Test brightness trait support for light domain.""" """Test brightness trait support for light domain."""
assert helpers.get_google_type(light.DOMAIN, None) is not None
assert trait.BrightnessTrait.supported(light.DOMAIN, assert trait.BrightnessTrait.supported(light.DOMAIN,
light.SUPPORT_BRIGHTNESS, None) light.SUPPORT_BRIGHTNESS, None)
@ -87,6 +88,7 @@ async def test_brightness_light(hass):
async def test_brightness_media_player(hass): async def test_brightness_media_player(hass):
"""Test brightness trait support for media player domain.""" """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, assert trait.BrightnessTrait.supported(media_player.DOMAIN,
media_player.SUPPORT_VOLUME_SET, media_player.SUPPORT_VOLUME_SET,
None) None)
@ -117,6 +119,7 @@ async def test_brightness_media_player(hass):
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')
assert helpers.get_google_type(camera.DOMAIN, None) is not None
assert trait.CameraStreamTrait.supported(camera.DOMAIN, assert trait.CameraStreamTrait.supported(camera.DOMAIN,
camera.SUPPORT_STREAM, None) camera.SUPPORT_STREAM, None)
@ -145,6 +148,7 @@ async def test_camera_stream(hass):
async def test_onoff_group(hass): async def test_onoff_group(hass):
"""Test OnOff trait support for group domain.""" """Test OnOff trait support for group domain."""
assert helpers.get_google_type(group.DOMAIN, None) is not None
assert trait.OnOffTrait.supported(group.DOMAIN, 0, None) assert trait.OnOffTrait.supported(group.DOMAIN, 0, None)
trt_on = trait.OnOffTrait(hass, State('group.bla', STATE_ON), BASIC_CONFIG) trt_on = trait.OnOffTrait(hass, State('group.bla', STATE_ON), BASIC_CONFIG)
@ -183,6 +187,7 @@ async def test_onoff_group(hass):
async def test_onoff_input_boolean(hass): async def test_onoff_input_boolean(hass):
"""Test OnOff trait support for input_boolean domain.""" """Test OnOff trait support for input_boolean domain."""
assert helpers.get_google_type(input_boolean.DOMAIN, None) is not None
assert trait.OnOffTrait.supported(input_boolean.DOMAIN, 0, None) assert trait.OnOffTrait.supported(input_boolean.DOMAIN, 0, None)
trt_on = trait.OnOffTrait(hass, State('input_boolean.bla', STATE_ON), trt_on = trait.OnOffTrait(hass, State('input_boolean.bla', STATE_ON),
@ -223,6 +228,7 @@ async def test_onoff_input_boolean(hass):
async def test_onoff_switch(hass): async def test_onoff_switch(hass):
"""Test OnOff trait support for switch domain.""" """Test OnOff trait support for switch domain."""
assert helpers.get_google_type(switch.DOMAIN, None) is not None
assert trait.OnOffTrait.supported(switch.DOMAIN, 0, None) assert trait.OnOffTrait.supported(switch.DOMAIN, 0, None)
trt_on = trait.OnOffTrait(hass, State('switch.bla', STATE_ON), trt_on = trait.OnOffTrait(hass, State('switch.bla', STATE_ON),
@ -262,6 +268,7 @@ async def test_onoff_switch(hass):
async def test_onoff_fan(hass): async def test_onoff_fan(hass):
"""Test OnOff trait support for fan domain.""" """Test OnOff trait support for fan domain."""
assert helpers.get_google_type(fan.DOMAIN, None) is not None
assert trait.OnOffTrait.supported(fan.DOMAIN, 0, None) assert trait.OnOffTrait.supported(fan.DOMAIN, 0, None)
trt_on = trait.OnOffTrait(hass, State('fan.bla', STATE_ON), BASIC_CONFIG) trt_on = trait.OnOffTrait(hass, State('fan.bla', STATE_ON), BASIC_CONFIG)
@ -298,6 +305,7 @@ async def test_onoff_fan(hass):
async def test_onoff_light(hass): async def test_onoff_light(hass):
"""Test OnOff trait support for light domain.""" """Test OnOff trait support for light domain."""
assert helpers.get_google_type(light.DOMAIN, None) is not None
assert trait.OnOffTrait.supported(light.DOMAIN, 0, None) assert trait.OnOffTrait.supported(light.DOMAIN, 0, None)
trt_on = trait.OnOffTrait(hass, State('light.bla', STATE_ON), BASIC_CONFIG) trt_on = trait.OnOffTrait(hass, State('light.bla', STATE_ON), BASIC_CONFIG)
@ -336,6 +344,7 @@ async def test_onoff_light(hass):
async def test_onoff_media_player(hass): async def test_onoff_media_player(hass):
"""Test OnOff trait support for media_player domain.""" """Test OnOff trait support for media_player domain."""
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
assert trait.OnOffTrait.supported(media_player.DOMAIN, 0, None) assert trait.OnOffTrait.supported(media_player.DOMAIN, 0, None)
trt_on = trait.OnOffTrait(hass, State('media_player.bla', STATE_ON), trt_on = trait.OnOffTrait(hass, State('media_player.bla', STATE_ON),
@ -377,12 +386,14 @@ async def test_onoff_media_player(hass):
async def test_onoff_climate(hass): async def test_onoff_climate(hass):
"""Test OnOff trait not supported for climate domain.""" """Test OnOff trait not supported for climate domain."""
assert helpers.get_google_type(climate.DOMAIN, None) is not None
assert not trait.OnOffTrait.supported( assert not trait.OnOffTrait.supported(
climate.DOMAIN, climate.SUPPORT_ON_OFF, None) climate.DOMAIN, climate.SUPPORT_ON_OFF, None)
async def test_dock_vacuum(hass): async def test_dock_vacuum(hass):
"""Test dock trait support for vacuum domain.""" """Test dock trait support for vacuum domain."""
assert helpers.get_google_type(vacuum.DOMAIN, None) is not None
assert trait.DockTrait.supported(vacuum.DOMAIN, 0, None) assert trait.DockTrait.supported(vacuum.DOMAIN, 0, None)
trt = trait.DockTrait(hass, State('vacuum.bla', vacuum.STATE_IDLE), trt = trait.DockTrait(hass, State('vacuum.bla', vacuum.STATE_IDLE),
@ -406,6 +417,7 @@ async def test_dock_vacuum(hass):
async def test_startstop_vacuum(hass): async def test_startstop_vacuum(hass):
"""Test startStop trait support for vacuum domain.""" """Test startStop trait support for vacuum domain."""
assert helpers.get_google_type(vacuum.DOMAIN, None) is not None
assert trait.StartStopTrait.supported(vacuum.DOMAIN, 0, None) assert trait.StartStopTrait.supported(vacuum.DOMAIN, 0, None)
trt = trait.StartStopTrait(hass, State('vacuum.bla', vacuum.STATE_PAUSED, { trt = trait.StartStopTrait(hass, State('vacuum.bla', vacuum.STATE_PAUSED, {
@ -454,6 +466,7 @@ async def test_startstop_vacuum(hass):
async def test_color_setting_color_light(hass): async def test_color_setting_color_light(hass):
"""Test ColorSpectrum trait support for light domain.""" """Test ColorSpectrum trait support for light domain."""
assert helpers.get_google_type(light.DOMAIN, None) is not None
assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None) assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None)
assert trait.ColorSettingTrait.supported(light.DOMAIN, assert trait.ColorSettingTrait.supported(light.DOMAIN,
light.SUPPORT_COLOR, None) light.SUPPORT_COLOR, None)
@ -515,6 +528,7 @@ async def test_color_setting_color_light(hass):
async def test_color_setting_temperature_light(hass): async def test_color_setting_temperature_light(hass):
"""Test ColorTemperature trait support for light domain.""" """Test ColorTemperature trait support for light domain."""
assert helpers.get_google_type(light.DOMAIN, None) is not None
assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None) assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None)
assert trait.ColorSettingTrait.supported(light.DOMAIN, assert trait.ColorSettingTrait.supported(light.DOMAIN,
light.SUPPORT_COLOR_TEMP, None) light.SUPPORT_COLOR_TEMP, None)
@ -568,6 +582,7 @@ async def test_color_setting_temperature_light(hass):
async def test_color_light_temperature_light_bad_temp(hass): async def test_color_light_temperature_light_bad_temp(hass):
"""Test ColorTemperature trait support for light domain.""" """Test ColorTemperature trait support for light domain."""
assert helpers.get_google_type(light.DOMAIN, None) is not None
assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None) assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None)
assert trait.ColorSettingTrait.supported(light.DOMAIN, assert trait.ColorSettingTrait.supported(light.DOMAIN,
light.SUPPORT_COLOR_TEMP, None) light.SUPPORT_COLOR_TEMP, None)
@ -584,6 +599,7 @@ async def test_color_light_temperature_light_bad_temp(hass):
async def test_scene_scene(hass): async def test_scene_scene(hass):
"""Test Scene trait support for scene domain.""" """Test Scene trait support for scene domain."""
assert helpers.get_google_type(scene.DOMAIN, None) is not None
assert trait.SceneTrait.supported(scene.DOMAIN, 0, None) assert trait.SceneTrait.supported(scene.DOMAIN, 0, None)
trt = trait.SceneTrait(hass, State('scene.bla', scene.STATE), BASIC_CONFIG) trt = trait.SceneTrait(hass, State('scene.bla', scene.STATE), BASIC_CONFIG)
@ -601,6 +617,7 @@ async def test_scene_scene(hass):
async def test_scene_script(hass): async def test_scene_script(hass):
"""Test Scene trait support for script domain.""" """Test Scene trait support for script domain."""
assert helpers.get_google_type(script.DOMAIN, None) is not None
assert trait.SceneTrait.supported(script.DOMAIN, 0, None) assert trait.SceneTrait.supported(script.DOMAIN, 0, None)
trt = trait.SceneTrait(hass, State('script.bla', STATE_OFF), BASIC_CONFIG) trt = trait.SceneTrait(hass, State('script.bla', STATE_OFF), BASIC_CONFIG)
@ -622,6 +639,7 @@ async def test_scene_script(hass):
async def test_temperature_setting_climate_onoff(hass): async def test_temperature_setting_climate_onoff(hass):
"""Test TemperatureSetting trait support for climate domain - range.""" """Test TemperatureSetting trait support for climate domain - range."""
assert helpers.get_google_type(climate.DOMAIN, None) is not None
assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None)
assert trait.TemperatureSettingTrait.supported( assert trait.TemperatureSettingTrait.supported(
climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None) climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None)
@ -666,6 +684,7 @@ async def test_temperature_setting_climate_onoff(hass):
async def test_temperature_setting_climate_range(hass): async def test_temperature_setting_climate_range(hass):
"""Test TemperatureSetting trait support for climate domain - range.""" """Test TemperatureSetting trait support for climate domain - range."""
assert helpers.get_google_type(climate.DOMAIN, None) is not None
assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None)
assert trait.TemperatureSettingTrait.supported( assert trait.TemperatureSettingTrait.supported(
climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None) climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None)
@ -741,6 +760,7 @@ async def test_temperature_setting_climate_range(hass):
async def test_temperature_setting_climate_setpoint(hass): async def test_temperature_setting_climate_setpoint(hass):
"""Test TemperatureSetting trait support for climate domain - setpoint.""" """Test TemperatureSetting trait support for climate domain - setpoint."""
assert helpers.get_google_type(climate.DOMAIN, None) is not None
assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None)
assert trait.TemperatureSettingTrait.supported( assert trait.TemperatureSettingTrait.supported(
climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None) climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None)
@ -841,6 +861,7 @@ async def test_temperature_setting_climate_setpoint_auto(hass):
async def test_lock_unlock_lock(hass): async def test_lock_unlock_lock(hass):
"""Test LockUnlock trait locking support for lock domain.""" """Test LockUnlock trait locking support for lock domain."""
assert helpers.get_google_type(lock.DOMAIN, None) is not None
assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN,
None) None)
@ -867,6 +888,7 @@ async def test_lock_unlock_lock(hass):
async def test_lock_unlock_unlock(hass): async def test_lock_unlock_unlock(hass):
"""Test LockUnlock trait unlocking support for lock domain.""" """Test LockUnlock trait unlocking support for lock domain."""
assert helpers.get_google_type(lock.DOMAIN, None) is not None
assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN,
None) None)
@ -905,6 +927,7 @@ async def test_lock_unlock_unlock(hass):
async def test_fan_speed(hass): async def test_fan_speed(hass):
"""Test FanSpeed trait speed control support for fan domain.""" """Test FanSpeed trait speed control support for fan domain."""
assert helpers.get_google_type(fan.DOMAIN, None) is not None
assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED,
None) None)
@ -988,6 +1011,7 @@ async def test_fan_speed(hass):
async def test_modes(hass): async def test_modes(hass):
"""Test Mode trait.""" """Test Mode trait."""
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
assert trait.ModesTrait.supported( assert trait.ModesTrait.supported(
media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE, None) media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE, None)
@ -1076,6 +1100,7 @@ async def test_modes(hass):
async def test_openclose_cover(hass): async def test_openclose_cover(hass):
"""Test OpenClose trait support for cover domain.""" """Test OpenClose trait support for cover domain."""
assert helpers.get_google_type(cover.DOMAIN, None) is not None
assert trait.OpenCloseTrait.supported(cover.DOMAIN, assert trait.OpenCloseTrait.supported(cover.DOMAIN,
cover.SUPPORT_SET_POSITION, None) cover.SUPPORT_SET_POSITION, None)
@ -1137,6 +1162,8 @@ async def test_openclose_cover(hass):
)) ))
async def test_openclose_binary_sensor(hass, device_class): async def test_openclose_binary_sensor(hass, device_class):
"""Test OpenClose trait support for binary_sensor domain.""" """Test OpenClose trait support for binary_sensor domain."""
assert helpers.get_google_type(
binary_sensor.DOMAIN, device_class) is not None
assert trait.OpenCloseTrait.supported(binary_sensor.DOMAIN, assert trait.OpenCloseTrait.supported(binary_sensor.DOMAIN,
0, device_class) 0, device_class)