Google Assistant for climate entities: Support QUERY and respect system-wide unit_system setting. (#10346)
This commit is contained in:
parent
309e493e76
commit
0b4de54725
4 changed files with 176 additions and 24 deletions
|
@ -81,7 +81,7 @@ class GoogleAssistantView(HomeAssistantView):
|
||||||
if not self.is_entity_exposed(entity):
|
if not self.is_entity_exposed(entity):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
device = entity_to_device(entity)
|
device = entity_to_device(entity, hass.config.units)
|
||||||
if device is None:
|
if device is None:
|
||||||
_LOGGER.warning("No mapping for %s domain", entity.domain)
|
_LOGGER.warning("No mapping for %s domain", entity.domain)
|
||||||
continue
|
continue
|
||||||
|
@ -89,9 +89,9 @@ class GoogleAssistantView(HomeAssistantView):
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
|
|
||||||
return self.json(
|
return self.json(
|
||||||
make_actions_response(request_id,
|
_make_actions_response(request_id,
|
||||||
{'agentUserId': self.agent_user_id,
|
{'agentUserId': self.agent_user_id,
|
||||||
'devices': devices}))
|
'devices': devices}))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def handle_query(self,
|
def handle_query(self,
|
||||||
|
@ -112,10 +112,10 @@ class GoogleAssistantView(HomeAssistantView):
|
||||||
# If we can't find a state, the device is offline
|
# If we can't find a state, the device is offline
|
||||||
devices[devid] = {'online': False}
|
devices[devid] = {'online': False}
|
||||||
|
|
||||||
devices[devid] = query_device(state)
|
devices[devid] = query_device(state, hass.config.units)
|
||||||
|
|
||||||
return self.json(
|
return self.json(
|
||||||
make_actions_response(request_id, {'devices': devices}))
|
_make_actions_response(request_id, {'devices': devices}))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def handle_execute(self,
|
def handle_execute(self,
|
||||||
|
@ -130,7 +130,8 @@ class GoogleAssistantView(HomeAssistantView):
|
||||||
for eid in ent_ids:
|
for eid in ent_ids:
|
||||||
domain = eid.split('.')[0]
|
domain = eid.split('.')[0]
|
||||||
(service, service_data) = determine_service(
|
(service, service_data) = determine_service(
|
||||||
eid, execution.get('command'), execution.get('params'))
|
eid, execution.get('command'), execution.get('params'),
|
||||||
|
hass.config.units)
|
||||||
success = yield from hass.services.async_call(
|
success = yield from hass.services.async_call(
|
||||||
domain, service, service_data, blocking=True)
|
domain, service, service_data, blocking=True)
|
||||||
result = {"ids": [eid], "states": {}}
|
result = {"ids": [eid], "states": {}}
|
||||||
|
@ -141,7 +142,7 @@ class GoogleAssistantView(HomeAssistantView):
|
||||||
commands.append(result)
|
commands.append(result)
|
||||||
|
|
||||||
return self.json(
|
return self.json(
|
||||||
make_actions_response(request_id, {'commands': commands}))
|
_make_actions_response(request_id, {'commands': commands}))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def post(self, request: Request) -> Response:
|
def post(self, request: Request) -> Response:
|
||||||
|
@ -181,6 +182,5 @@ class GoogleAssistantView(HomeAssistantView):
|
||||||
"invalid intent", status_code=HTTP_BAD_REQUEST)
|
"invalid intent", status_code=HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
def make_actions_response(request_id: str, payload: dict) -> dict:
|
def _make_actions_response(request_id: str, payload: dict) -> dict:
|
||||||
"""Helper to simplify format for response."""
|
|
||||||
return {'requestId': request_id, 'payload': payload}
|
return {'requestId': request_id, 'payload': payload}
|
||||||
|
|
|
@ -5,18 +5,21 @@ import logging
|
||||||
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
|
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
|
||||||
# if False:
|
# if False:
|
||||||
from aiohttp.web import Request, Response # NOQA
|
from aiohttp.web import Request, Response # NOQA
|
||||||
from typing import Dict, Tuple, Any # NOQA
|
from typing import Dict, Tuple, Any, Optional # NOQA
|
||||||
from homeassistant.helpers.entity import Entity # NOQA
|
from homeassistant.helpers.entity import Entity # NOQA
|
||||||
from homeassistant.core import HomeAssistant # NOQA
|
from homeassistant.core import HomeAssistant # NOQA
|
||||||
|
from homeassistant.util.unit_system import UnitSystem # NOQA
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID,
|
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID,
|
||||||
CONF_FRIENDLY_NAME, STATE_OFF,
|
CONF_FRIENDLY_NAME, STATE_OFF,
|
||||||
SERVICE_TURN_OFF, SERVICE_TURN_ON
|
SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
|
TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||||
)
|
)
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
switch, light, cover, media_player, group, fan, scene, script, climate
|
switch, light, cover, media_player, group, fan, scene, script, climate
|
||||||
)
|
)
|
||||||
|
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_GOOGLE_ASSISTANT_NAME, ATTR_GOOGLE_ASSISTANT_TYPE,
|
ATTR_GOOGLE_ASSISTANT_NAME, ATTR_GOOGLE_ASSISTANT_TYPE,
|
||||||
|
@ -65,7 +68,7 @@ def make_actions_response(request_id: str, payload: dict) -> dict:
|
||||||
return {'requestId': request_id, 'payload': payload}
|
return {'requestId': request_id, 'payload': payload}
|
||||||
|
|
||||||
|
|
||||||
def entity_to_device(entity: Entity):
|
def entity_to_device(entity: Entity, units: UnitSystem):
|
||||||
"""Convert a hass entity into an google actions device."""
|
"""Convert a hass entity into an google actions device."""
|
||||||
class_data = MAPPING_COMPONENT.get(
|
class_data = MAPPING_COMPONENT.get(
|
||||||
entity.attributes.get(ATTR_GOOGLE_ASSISTANT_TYPE) or entity.domain)
|
entity.attributes.get(ATTR_GOOGLE_ASSISTANT_TYPE) or entity.domain)
|
||||||
|
@ -105,14 +108,39 @@ def entity_to_device(entity: Entity):
|
||||||
if m in CLIMATE_SUPPORTED_MODES)
|
if m in CLIMATE_SUPPORTED_MODES)
|
||||||
device['attributes'] = {
|
device['attributes'] = {
|
||||||
'availableThermostatModes': modes,
|
'availableThermostatModes': modes,
|
||||||
'thermostatTemperatureUnit': 'C',
|
'thermostatTemperatureUnit':
|
||||||
|
'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C',
|
||||||
}
|
}
|
||||||
|
|
||||||
return device
|
return device
|
||||||
|
|
||||||
|
|
||||||
def query_device(entity: Entity) -> dict:
|
def query_device(entity: Entity, units: UnitSystem) -> dict:
|
||||||
"""Take an entity and return a properly formatted device object."""
|
"""Take an entity and return a properly formatted device object."""
|
||||||
|
def celsius(deg: Optional[float]) -> Optional[float]:
|
||||||
|
"""Convert a float to Celsius and rounds to one decimal place."""
|
||||||
|
if deg is None:
|
||||||
|
return None
|
||||||
|
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
|
||||||
|
if entity.domain == climate.DOMAIN:
|
||||||
|
mode = entity.attributes.get(climate.ATTR_OPERATION_MODE)
|
||||||
|
if mode not in CLIMATE_SUPPORTED_MODES:
|
||||||
|
mode = 'on'
|
||||||
|
response = {
|
||||||
|
'thermostatMode': mode,
|
||||||
|
'thermostatTemperatureSetpoint':
|
||||||
|
celsius(entity.attributes.get(climate.ATTR_TEMPERATURE)),
|
||||||
|
'thermostatTemperatureAmbient':
|
||||||
|
celsius(entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)),
|
||||||
|
'thermostatTemperatureSetpointHigh':
|
||||||
|
celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)),
|
||||||
|
'thermostatTemperatureSetpointLow':
|
||||||
|
celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)),
|
||||||
|
'thermostatHumidityAmbient':
|
||||||
|
entity.attributes.get(climate.ATTR_CURRENT_HUMIDITY),
|
||||||
|
}
|
||||||
|
return {k: v for k, v in response.items() if v is not None}
|
||||||
|
|
||||||
final_state = entity.state != STATE_OFF
|
final_state = entity.state != STATE_OFF
|
||||||
final_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255
|
final_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255
|
||||||
if final_state else 0)
|
if final_state else 0)
|
||||||
|
@ -138,8 +166,9 @@ def query_device(entity: Entity) -> dict:
|
||||||
# erroneous bug on old pythons and pylint
|
# erroneous bug on old pythons and pylint
|
||||||
# https://github.com/PyCQA/pylint/issues/1212
|
# https://github.com/PyCQA/pylint/issues/1212
|
||||||
# pylint: disable=invalid-sequence-index
|
# pylint: disable=invalid-sequence-index
|
||||||
def determine_service(entity_id: str, command: str,
|
def determine_service(
|
||||||
params: dict) -> Tuple[str, dict]:
|
entity_id: str, command: str, params: dict,
|
||||||
|
units: UnitSystem) -> Tuple[str, dict]:
|
||||||
"""
|
"""
|
||||||
Determine service and service_data.
|
Determine service and service_data.
|
||||||
|
|
||||||
|
@ -166,14 +195,17 @@ def determine_service(entity_id: str, command: str,
|
||||||
# special climate handling
|
# special climate handling
|
||||||
if domain == climate.DOMAIN:
|
if domain == climate.DOMAIN:
|
||||||
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
|
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
|
||||||
service_data['temperature'] = params.get(
|
service_data['temperature'] = units.temperature(
|
||||||
'thermostatTemperatureSetpoint', 25)
|
params.get('thermostatTemperatureSetpoint', 25),
|
||||||
|
TEMP_CELSIUS)
|
||||||
return (climate.SERVICE_SET_TEMPERATURE, service_data)
|
return (climate.SERVICE_SET_TEMPERATURE, service_data)
|
||||||
if command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE:
|
if command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE:
|
||||||
service_data['target_temp_high'] = params.get(
|
service_data['target_temp_high'] = units.temperature(
|
||||||
'thermostatTemperatureSetpointHigh', 25)
|
params.get('thermostatTemperatureSetpointHigh', 25),
|
||||||
service_data['target_temp_low'] = params.get(
|
TEMP_CELSIUS)
|
||||||
'thermostatTemperatureSetpointLow', 18)
|
service_data['target_temp_low'] = units.temperature(
|
||||||
|
params.get('thermostatTemperatureSetpointLow', 18),
|
||||||
|
TEMP_CELSIUS)
|
||||||
return (climate.SERVICE_SET_TEMPERATURE, service_data)
|
return (climate.SERVICE_SET_TEMPERATURE, service_data)
|
||||||
if command == COMMAND_THERMOSTAT_SET_MODE:
|
if command == COMMAND_THERMOSTAT_SET_MODE:
|
||||||
service_data['operation_mode'] = params.get(
|
service_data['operation_mode'] = params.get(
|
||||||
|
|
|
@ -11,6 +11,7 @@ from homeassistant import core, const, setup
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
fan, http, cover, light, switch, climate, async_setup, media_player)
|
fan, http, cover, light, switch, climate, async_setup, media_player)
|
||||||
from homeassistant.components import google_assistant as ga
|
from homeassistant.components import google_assistant as ga
|
||||||
|
from homeassistant.util.unit_system import IMPERIAL_SYSTEM
|
||||||
|
|
||||||
from . import DEMO_DEVICES
|
from . import DEMO_DEVICES
|
||||||
|
|
||||||
|
@ -198,6 +199,101 @@ def test_query_request(hass_fixture, assistant_client):
|
||||||
assert devices['light.ceiling_lights']['brightness'] == 70
|
assert devices['light.ceiling_lights']['brightness'] == 70
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_query_climate_request(hass_fixture, assistant_client):
|
||||||
|
"""Test a query request."""
|
||||||
|
reqid = '5711642932632160984'
|
||||||
|
data = {
|
||||||
|
'requestId':
|
||||||
|
reqid,
|
||||||
|
'inputs': [{
|
||||||
|
'intent': 'action.devices.QUERY',
|
||||||
|
'payload': {
|
||||||
|
'devices': [
|
||||||
|
{'id': 'climate.hvac'},
|
||||||
|
{'id': 'climate.heatpump'},
|
||||||
|
{'id': 'climate.ecobee'},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
result = yield from assistant_client.post(
|
||||||
|
ga.const.GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=AUTH_HEADER)
|
||||||
|
assert result.status == 200
|
||||||
|
body = yield from result.json()
|
||||||
|
assert body.get('requestId') == reqid
|
||||||
|
devices = body['payload']['devices']
|
||||||
|
assert devices == {
|
||||||
|
'climate.heatpump': {
|
||||||
|
'thermostatTemperatureSetpoint': 20.0,
|
||||||
|
'thermostatTemperatureAmbient': 25.0,
|
||||||
|
'thermostatMode': 'heat',
|
||||||
|
},
|
||||||
|
'climate.ecobee': {
|
||||||
|
'thermostatTemperatureSetpointHigh': 24,
|
||||||
|
'thermostatTemperatureAmbient': 23,
|
||||||
|
'thermostatMode': 'on',
|
||||||
|
'thermostatTemperatureSetpointLow': 21
|
||||||
|
},
|
||||||
|
'climate.hvac': {
|
||||||
|
'thermostatTemperatureSetpoint': 21,
|
||||||
|
'thermostatTemperatureAmbient': 22,
|
||||||
|
'thermostatMode': 'cool',
|
||||||
|
'thermostatHumidityAmbient': 54,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_query_climate_request_f(hass_fixture, assistant_client):
|
||||||
|
"""Test a query request."""
|
||||||
|
hass_fixture.config.units = IMPERIAL_SYSTEM
|
||||||
|
reqid = '5711642932632160984'
|
||||||
|
data = {
|
||||||
|
'requestId':
|
||||||
|
reqid,
|
||||||
|
'inputs': [{
|
||||||
|
'intent': 'action.devices.QUERY',
|
||||||
|
'payload': {
|
||||||
|
'devices': [
|
||||||
|
{'id': 'climate.hvac'},
|
||||||
|
{'id': 'climate.heatpump'},
|
||||||
|
{'id': 'climate.ecobee'},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
result = yield from assistant_client.post(
|
||||||
|
ga.const.GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=AUTH_HEADER)
|
||||||
|
assert result.status == 200
|
||||||
|
body = yield from result.json()
|
||||||
|
assert body.get('requestId') == reqid
|
||||||
|
devices = body['payload']['devices']
|
||||||
|
assert devices == {
|
||||||
|
'climate.heatpump': {
|
||||||
|
'thermostatTemperatureSetpoint': -6.7,
|
||||||
|
'thermostatTemperatureAmbient': -3.9,
|
||||||
|
'thermostatMode': 'heat',
|
||||||
|
},
|
||||||
|
'climate.ecobee': {
|
||||||
|
'thermostatTemperatureSetpointHigh': -4.4,
|
||||||
|
'thermostatTemperatureAmbient': -5,
|
||||||
|
'thermostatMode': 'on',
|
||||||
|
'thermostatTemperatureSetpointLow': -6.1,
|
||||||
|
},
|
||||||
|
'climate.hvac': {
|
||||||
|
'thermostatTemperatureSetpoint': -6.1,
|
||||||
|
'thermostatTemperatureAmbient': -5.6,
|
||||||
|
'thermostatMode': 'cool',
|
||||||
|
'thermostatHumidityAmbient': 54,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_execute_request(hass_fixture, assistant_client):
|
def test_execute_request(hass_fixture, assistant_client):
|
||||||
"""Test a execute request."""
|
"""Test a execute request."""
|
||||||
|
|
|
@ -5,6 +5,7 @@ import asyncio
|
||||||
from homeassistant import const
|
from homeassistant import const
|
||||||
from homeassistant.components import climate
|
from homeassistant.components import climate
|
||||||
from homeassistant.components import google_assistant as ga
|
from homeassistant.components import google_assistant as ga
|
||||||
|
from homeassistant.util.unit_system import (IMPERIAL_SYSTEM, METRIC_SYSTEM)
|
||||||
|
|
||||||
DETERMINE_SERVICE_TESTS = [{ # Test light brightness
|
DETERMINE_SERVICE_TESTS = [{ # Test light brightness
|
||||||
'entity_id': 'light.test',
|
'entity_id': 'light.test',
|
||||||
|
@ -82,6 +83,15 @@ DETERMINE_SERVICE_TESTS = [{ # Test light brightness
|
||||||
climate.SERVICE_SET_TEMPERATURE,
|
climate.SERVICE_SET_TEMPERATURE,
|
||||||
{'entity_id': 'climate.living_room', 'temperature': 24.5}
|
{'entity_id': 'climate.living_room', 'temperature': 24.5}
|
||||||
),
|
),
|
||||||
|
}, { # Test climate temperature Fahrenheit
|
||||||
|
'entity_id': 'climate.living_room',
|
||||||
|
'command': ga.const.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
|
||||||
|
'params': {'thermostatTemperatureSetpoint': 24.5},
|
||||||
|
'units': IMPERIAL_SYSTEM,
|
||||||
|
'expected': (
|
||||||
|
climate.SERVICE_SET_TEMPERATURE,
|
||||||
|
{'entity_id': 'climate.living_room', 'temperature': 76.1}
|
||||||
|
),
|
||||||
}, { # Test climate temperature range
|
}, { # Test climate temperature range
|
||||||
'entity_id': 'climate.living_room',
|
'entity_id': 'climate.living_room',
|
||||||
'command': ga.const.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
|
'command': ga.const.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
|
||||||
|
@ -94,6 +104,19 @@ DETERMINE_SERVICE_TESTS = [{ # Test light brightness
|
||||||
{'entity_id': 'climate.living_room',
|
{'entity_id': 'climate.living_room',
|
||||||
'target_temp_high': 24.5, 'target_temp_low': 20.5}
|
'target_temp_high': 24.5, 'target_temp_low': 20.5}
|
||||||
),
|
),
|
||||||
|
}, { # Test climate temperature range Fahrenheit
|
||||||
|
'entity_id': 'climate.living_room',
|
||||||
|
'command': ga.const.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
|
||||||
|
'params': {
|
||||||
|
'thermostatTemperatureSetpointHigh': 24.5,
|
||||||
|
'thermostatTemperatureSetpointLow': 20.5,
|
||||||
|
},
|
||||||
|
'units': IMPERIAL_SYSTEM,
|
||||||
|
'expected': (
|
||||||
|
climate.SERVICE_SET_TEMPERATURE,
|
||||||
|
{'entity_id': 'climate.living_room',
|
||||||
|
'target_temp_high': 76.1, 'target_temp_low': 68.9}
|
||||||
|
),
|
||||||
}, { # Test climate operation mode
|
}, { # Test climate operation mode
|
||||||
'entity_id': 'climate.living_room',
|
'entity_id': 'climate.living_room',
|
||||||
'command': ga.const.COMMAND_THERMOSTAT_SET_MODE,
|
'command': ga.const.COMMAND_THERMOSTAT_SET_MODE,
|
||||||
|
@ -122,5 +145,6 @@ def test_determine_service():
|
||||||
result = ga.smart_home.determine_service(
|
result = ga.smart_home.determine_service(
|
||||||
test['entity_id'],
|
test['entity_id'],
|
||||||
test['command'],
|
test['command'],
|
||||||
test['params'])
|
test['params'],
|
||||||
|
test.get('units', METRIC_SYSTEM))
|
||||||
assert result == test['expected']
|
assert result == test['expected']
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue