From 4a2a130bfac2178f1c562c6dcb2e5d1b1ac8f903 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 18 Apr 2019 07:37:39 +0200 Subject: [PATCH] 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 --- homeassistant/components/cloud/http_api.py | 5 +- .../components/google_assistant/const.py | 46 ++++ .../components/google_assistant/error.py | 13 + .../components/google_assistant/helpers.py | 195 ++++++++++++++- .../components/google_assistant/smart_home.py | 229 +----------------- .../components/google_assistant/trait.py | 2 +- tests/components/cloud/test_http_api.py | 2 +- .../google_assistant/test_smart_home.py | 6 +- .../components/google_assistant/test_trait.py | 27 +++ 9 files changed, 285 insertions(+), 240 deletions(-) create mode 100644 homeassistant/components/google_assistant/error.py diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index fe13172d7fe..6ab7d911d47 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -14,7 +14,8 @@ from homeassistant.components.http.data_validator import ( RequestDataValidator) from homeassistant.components import websocket_api 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 ( DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, @@ -415,7 +416,7 @@ def _account_data(cloud): 'cloud': cloud.iot.state, 'prefs': client.prefs.as_dict(), '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_domains': list(alexa_sh.ENTITY_ADAPTERS), 'remote_domain': remote.instance_domain, diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 477e67ab75a..67c767c080b 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -1,4 +1,20 @@ """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' GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant' @@ -32,6 +48,7 @@ TYPE_LOCK = PREFIX_TYPES + 'LOCK' TYPE_BLINDS = PREFIX_TYPES + 'BLINDS' TYPE_GARAGE = PREFIX_TYPES + 'GARAGE' TYPE_OUTLET = PREFIX_TYPES + 'OUTLET' +TYPE_SENSOR = PREFIX_TYPES + 'SENSOR' SERVICE_REQUEST_SYNC = 'request_sync' HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' @@ -51,3 +68,32 @@ ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported' EVENT_COMMAND_RECEIVED = 'google_assistant_command' EVENT_QUERY_RECEIVED = 'google_assistant_query' 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, + +} diff --git a/homeassistant/components/google_assistant/error.py b/homeassistant/components/google_assistant/error.py new file mode 100644 index 00000000000..2225bb58242 --- /dev/null +++ b/homeassistant/components/google_assistant/error.py @@ -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 diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 8afa55acc5c..982b840393e 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -1,17 +1,19 @@ """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): - """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 +from . import trait +from .const import ( + DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED, + DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT, +) +from .error import SmartHomeError class Config: @@ -33,3 +35,174 @@ class RequestData: self.config = config self.request_id = request_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 diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index ab2907cf661..9edde36f09d 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -1,237 +1,22 @@ """Support for Google Assistant Smart Home API.""" -from asyncio import gather -from collections.abc import Mapping from itertools import product import logging from homeassistant.util.decorator import Registry -from homeassistant.core import callback from homeassistant.const import ( - CLOUD_NEVER_EXPOSED_ENTITIES, CONF_NAME, STATE_UNAVAILABLE, - 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, -) + CLOUD_NEVER_EXPOSED_ENTITIES, ATTR_ENTITY_ID) - -from . import trait from .const import ( - TYPE_LIGHT, TYPE_LOCK, TYPE_SCENE, TYPE_SWITCH, TYPE_VACUUM, - 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, + ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR, 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() _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): """Handle incoming API messages.""" @@ -304,7 +89,7 @@ async def async_devices_sync(hass, data, payload): if not data.config.should_expose(state): continue - entity = _GoogleEntity(hass, data.config, state) + entity = GoogleEntity(hass, data.config, state) serialized = await entity.sync_serialize() if serialized is None: @@ -345,7 +130,7 @@ async def async_devices_query(hass, data, payload): devices[devid] = {'online': False} continue - entity = _GoogleEntity(hass, data.config, state) + entity = GoogleEntity(hass, data.config, state) devices[devid] = entity.query_serialize() return {'devices': devices} @@ -389,7 +174,7 @@ async def handle_devices_execute(hass, data, payload): } continue - entities[entity_id] = _GoogleEntity(hass, data.config, state) + entities[entity_id] = GoogleEntity(hass, data.config, state) try: await entities[entity_id].execute(execution['command'], diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index a79dfdd3dca..5bec683ccc7 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -38,7 +38,7 @@ from .const import ( ERR_NOT_SUPPORTED, ERR_FUNCTION_NOT_SUPPORTED, ) -from .helpers import SmartHomeError +from .error import SmartHomeError _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 6c50a158cad..c147f8492d7 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -335,7 +335,7 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture, client = await hass_ws_client(hass) with patch.dict( - 'homeassistant.components.google_assistant.smart_home.' + 'homeassistant.components.google_assistant.const.' 'DOMAIN_TO_GOOGLE_TYPES', {'light': None}, clear=True ), patch.dict('homeassistant.components.alexa.smart_home.ENTITY_ADAPTERS', {'switch': None}, clear=True): diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 24c7059d5c5..30a398fccc3 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -96,7 +96,7 @@ async def test_sync_message(hass): trait.TRAIT_ONOFF, trait.TRAIT_COLOR_SETTING, ], - 'type': sh.TYPE_LIGHT, + 'type': const.TYPE_LIGHT, 'willReportState': False, 'attributes': { 'colorModel': 'hsv', @@ -176,7 +176,7 @@ async def test_sync_in_area(hass, registries): trait.TRAIT_ONOFF, trait.TRAIT_COLOR_SETTING, ], - 'type': sh.TYPE_LIGHT, + 'type': const.TYPE_LIGHT, 'willReportState': False, 'attributes': { 'colorModel': 'hsv', @@ -489,7 +489,7 @@ async def test_serialize_input_boolean(hass): """Test serializing an input boolean entity.""" state = State('input_boolean.bla', 'on') # pylint: disable=protected-access - entity = sh._GoogleEntity(hass, BASIC_CONFIG, state) + entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) result = await entity.sync_serialize() assert result == { 'id': 'input_boolean.bla', diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 07db4c47296..12731978f57 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -49,6 +49,7 @@ UNSAFE_CONFIG = helpers.Config( async def test_brightness_light(hass): """Test brightness trait support for light domain.""" + assert helpers.get_google_type(light.DOMAIN, None) is not None assert trait.BrightnessTrait.supported(light.DOMAIN, light.SUPPORT_BRIGHTNESS, None) @@ -87,6 +88,7 @@ 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) @@ -117,6 +119,7 @@ async def test_brightness_media_player(hass): 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') + assert helpers.get_google_type(camera.DOMAIN, None) is not None assert trait.CameraStreamTrait.supported(camera.DOMAIN, camera.SUPPORT_STREAM, None) @@ -145,6 +148,7 @@ async def test_camera_stream(hass): async def test_onoff_group(hass): """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) 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): """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) 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): """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) 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): """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) 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): """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) 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): """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) 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): """Test OnOff trait not supported for climate domain.""" + assert helpers.get_google_type(climate.DOMAIN, None) is not None assert not trait.OnOffTrait.supported( climate.DOMAIN, climate.SUPPORT_ON_OFF, None) async def test_dock_vacuum(hass): """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) 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): """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) 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): """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 trait.ColorSettingTrait.supported(light.DOMAIN, light.SUPPORT_COLOR, None) @@ -515,6 +528,7 @@ async def test_color_setting_color_light(hass): async def test_color_setting_temperature_light(hass): """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 trait.ColorSettingTrait.supported(light.DOMAIN, 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): """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 trait.ColorSettingTrait.supported(light.DOMAIN, 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): """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) 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): """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) 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): """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 trait.TemperatureSettingTrait.supported( 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): """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 trait.TemperatureSettingTrait.supported( 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): """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 trait.TemperatureSettingTrait.supported( 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): """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, None) @@ -867,6 +888,7 @@ async def test_lock_unlock_lock(hass): async def test_lock_unlock_unlock(hass): """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, None) @@ -905,6 +927,7 @@ async def test_lock_unlock_unlock(hass): async def test_fan_speed(hass): """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, None) @@ -988,6 +1011,7 @@ async def test_fan_speed(hass): async def test_modes(hass): """Test Mode trait.""" + assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.ModesTrait.supported( media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE, None) @@ -1076,6 +1100,7 @@ async def test_modes(hass): async def test_openclose_cover(hass): """Test OpenClose trait support for cover domain.""" + assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported(cover.DOMAIN, cover.SUPPORT_SET_POSITION, None) @@ -1137,6 +1162,8 @@ async def test_openclose_cover(hass): )) async def test_openclose_binary_sensor(hass, device_class): """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, 0, device_class)