Skip to content

Commit

Permalink
Google Assistant for climate entities: Support QUERY and respect syst…
Browse files Browse the repository at this point in the history
…em-wide unit_system setting.
  • Loading branch information
emosenkis committed Nov 14, 2017
1 parent 95c831d commit 0c747d5
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 24 deletions.
20 changes: 10 additions & 10 deletions homeassistant/components/google_assistant/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,17 @@ def handle_sync(self, hass: HomeAssistant, request_id: str):
if not self.is_entity_exposed(entity):
continue

device = entity_to_device(entity)
device = entity_to_device(entity, hass.config.units)
if device is None:
_LOGGER.warning("No mapping for %s domain", entity.domain)
continue

devices.append(device)

return self.json(
make_actions_response(request_id,
{'agentUserId': self.agent_user_id,
'devices': devices}))
_make_actions_response(request_id,
{'agentUserId': self.agent_user_id,
'devices': devices}))

@asyncio.coroutine
def handle_query(self,
Expand All @@ -112,10 +112,10 @@ def handle_query(self,
# If we can't find a state, the device is offline
devices[devid] = {'online': False}

devices[devid] = query_device(state)
devices[devid] = query_device(state, hass.config.units)

return self.json(
make_actions_response(request_id, {'devices': devices}))
_make_actions_response(request_id, {'devices': devices}))

@asyncio.coroutine
def handle_execute(self,
Expand All @@ -130,7 +130,8 @@ def handle_execute(self,
for eid in ent_ids:
domain = eid.split('.')[0]
(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(
domain, service, service_data, blocking=True)
result = {"ids": [eid], "states": {}}
Expand All @@ -141,7 +142,7 @@ def handle_execute(self,
commands.append(result)

return self.json(
make_actions_response(request_id, {'commands': commands}))
_make_actions_response(request_id, {'commands': commands}))

@asyncio.coroutine
def post(self, request: Request) -> Response:
Expand Down Expand Up @@ -181,6 +182,5 @@ def post(self, request: Request) -> Response:
"invalid intent", status_code=HTTP_BAD_REQUEST)


def make_actions_response(request_id: str, payload: dict) -> dict:
"""Helper to simplify format for response."""
def _make_actions_response(request_id: str, payload: dict) -> dict:
return {'requestId': request_id, 'payload': payload}
58 changes: 45 additions & 13 deletions homeassistant/components/google_assistant/smart_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
# if False:
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.core import HomeAssistant # NOQA
from homeassistant.util.unit_system import UnitSystem # NOQA

from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID,
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 (
switch, light, cover, media_player, group, fan, scene, script, climate
)
from homeassistant.util.unit_system import METRIC_SYSTEM

from .const import (
ATTR_GOOGLE_ASSISTANT_NAME, ATTR_GOOGLE_ASSISTANT_TYPE,
Expand Down Expand Up @@ -65,7 +68,7 @@ def make_actions_response(request_id: str, payload: dict) -> dict:
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."""
class_data = MAPPING_COMPONENT.get(
entity.attributes.get(ATTR_GOOGLE_ASSISTANT_TYPE) or entity.domain)
Expand Down Expand Up @@ -105,14 +108,39 @@ def entity_to_device(entity: Entity):
if m in CLIMATE_SUPPORTED_MODES)
device['attributes'] = {
'availableThermostatModes': modes,
'thermostatTemperatureUnit': 'C',
'thermostatTemperatureUnit':
'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C',
}

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."""
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_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255
if final_state else 0)
Expand All @@ -138,8 +166,9 @@ def query_device(entity: Entity) -> dict:
# erroneous bug on old pythons and pylint
# https://github.com/PyCQA/pylint/issues/1212
# pylint: disable=invalid-sequence-index
def determine_service(entity_id: str, command: str,
params: dict) -> Tuple[str, dict]:
def determine_service(
entity_id: str, command: str, params: dict,
units: UnitSystem) -> Tuple[str, dict]:
"""
Determine service and service_data.
Expand All @@ -166,14 +195,17 @@ def determine_service(entity_id: str, command: str,
# special climate handling
if domain == climate.DOMAIN:
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
service_data['temperature'] = params.get(
'thermostatTemperatureSetpoint', 25)
service_data['temperature'] = units.temperature(
params.get('thermostatTemperatureSetpoint', 25),
TEMP_CELSIUS)
return (climate.SERVICE_SET_TEMPERATURE, service_data)
if command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE:
service_data['target_temp_high'] = params.get(
'thermostatTemperatureSetpointHigh', 25)
service_data['target_temp_low'] = params.get(
'thermostatTemperatureSetpointLow', 18)
service_data['target_temp_high'] = units.temperature(
params.get('thermostatTemperatureSetpointHigh', 25),
TEMP_CELSIUS)
service_data['target_temp_low'] = units.temperature(
params.get('thermostatTemperatureSetpointLow', 18),
TEMP_CELSIUS)
return (climate.SERVICE_SET_TEMPERATURE, service_data)
if command == COMMAND_THERMOSTAT_SET_MODE:
service_data['operation_mode'] = params.get(
Expand Down
96 changes: 96 additions & 0 deletions tests/components/google_assistant/test_google_assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from homeassistant.components import (
fan, http, cover, light, switch, climate, async_setup, media_player)
from homeassistant.components import google_assistant as ga
from homeassistant.util.unit_system import IMPERIAL_SYSTEM

from . import DEMO_DEVICES

Expand Down Expand Up @@ -198,6 +199,101 @@ def test_query_request(hass_fixture, assistant_client):
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
def test_execute_request(hass_fixture, assistant_client):
"""Test a execute request."""
Expand Down
26 changes: 25 additions & 1 deletion tests/components/google_assistant/test_smart_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from homeassistant import const
from homeassistant.components import climate
from homeassistant.components import google_assistant as ga
from homeassistant.util.unit_system import (IMPERIAL_SYSTEM, METRIC_SYSTEM)

DETERMINE_SERVICE_TESTS = [{ # Test light brightness
'entity_id': 'light.test',
Expand Down Expand Up @@ -82,6 +83,15 @@
climate.SERVICE_SET_TEMPERATURE,
{'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
'entity_id': 'climate.living_room',
'command': ga.const.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
Expand All @@ -94,6 +104,19 @@
{'entity_id': 'climate.living_room',
'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
'entity_id': 'climate.living_room',
'command': ga.const.COMMAND_THERMOSTAT_SET_MODE,
Expand Down Expand Up @@ -122,5 +145,6 @@ def test_determine_service():
result = ga.smart_home.determine_service(
test['entity_id'],
test['command'],
test['params'])
test['params'],
test.get('units', METRIC_SYSTEM))
assert result == test['expected']

0 comments on commit 0c747d5

Please sign in to comment.