mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
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:
parent
ce8ec3acb1
commit
4a2a130bfa
@ -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,
|
||||
|
@ -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,
|
||||
|
||||
}
|
||||
|
13
homeassistant/components/google_assistant/error.py
Normal file
13
homeassistant/components/google_assistant/error.py
Normal 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
|
@ -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
|
||||
|
@ -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'],
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user