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
9 changed files with 285 additions and 240 deletions
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
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."""
|
"""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
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -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__)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue