diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index f9d6135107b2e8ffd3a70c1907c6ad100fb2f714..e2ad609a007e34381b79071de564a943b9a8c54b 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -14,13 +14,14 @@ from homeassistant.util.unit_system import UnitSystem # NOQA from homeassistant.util.decorator import Registry from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, TEMP_FAHRENHEIT, TEMP_CELSIUS, CONF_NAME, CONF_TYPE ) from homeassistant.components import ( - switch, light, cover, media_player, group, fan, scene, script, climate + switch, light, cover, media_player, group, fan, scene, script, climate, + sensor ) from homeassistant.util.unit_system import METRIC_SYSTEM @@ -67,6 +68,23 @@ MAPPING_COMPONENT = { } # type: Dict[str, list] +"""Error code used for SmartHomeError class.""" +ERROR_NOT_SUPPORTED = "notSupported" + + +class SmartHomeError(Exception): + """Google Assistant Smart Home errors.""" + + def __init__(self, code, msg): + """Log error code.""" + super(SmartHomeError, self).__init__(msg) + _LOGGER.error( + "An error has ocurred in Google SmartHome: %s." + "Error code: %s", msg, code + ) + self.code = code + + class Config: """Hold the configuration for Google Assistant.""" @@ -80,8 +98,9 @@ class Config: def entity_to_device(entity: Entity, config: Config, units: UnitSystem): """Convert a hass entity into an google actions device.""" entity_config = config.entity_config.get(entity.entity_id, {}) + google_domain = entity_config.get(CONF_TYPE) class_data = MAPPING_COMPONENT.get( - entity_config.get(CONF_TYPE) or entity.domain) + google_domain or entity.domain) if class_data is None: return None @@ -138,16 +157,75 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem): 'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C', } _LOGGER.debug('Thermostat attributes %s', device['attributes']) + + if entity.domain == sensor.DOMAIN: + if google_domain == climate.DOMAIN: + unit_of_measurement = entity.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, + units.temperature_unit + ) + + device['attributes'] = { + 'thermostatTemperatureUnit': + 'F' if unit_of_measurement == TEMP_FAHRENHEIT else 'C', + } + _LOGGER.debug('Sensor attributes %s', device['attributes']) + return device -def query_device(entity: Entity, units: UnitSystem) -> dict: +def query_device(entity: Entity, config: Config, 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 == sensor.DOMAIN: + entity_config = config.entity_config.get(entity.entity_id, {}) + google_domain = entity_config.get(CONF_TYPE) + + if google_domain == climate.DOMAIN: + # check if we have a string value to convert it to number + value = entity.state + if isinstance(entity.state, str): + try: + value = float(value) + except ValueError: + value = None + + if value is None: + raise SmartHomeError( + ERROR_NOT_SUPPORTED, + "Invalid value {} for the climate sensor" + .format(entity.state) + ) + + # detect if we report temperature or humidity + unit_of_measurement = entity.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, + units.temperature_unit + ) + if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]: + value = celsius(value) + attr = 'thermostatTemperatureAmbient' + elif unit_of_measurement == '%': + attr = 'thermostatHumidityAmbient' + else: + raise SmartHomeError( + ERROR_NOT_SUPPORTED, + "Unit {} is not supported by the climate sensor" + .format(unit_of_measurement) + ) + + return {attr: value} + + raise SmartHomeError( + ERROR_NOT_SUPPORTED, + "Sensor type {} is not supported".format(google_domain) + ) + if entity.domain == climate.DOMAIN: mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower() if mode not in CLIMATE_SUPPORTED_MODES: @@ -317,7 +395,7 @@ def async_handle_message(hass, config, message): @HANDLERS.register('action.devices.SYNC') @asyncio.coroutine -def async_devices_sync(hass, config, payload): +def async_devices_sync(hass, config: Config, payload): """Handle action.devices.SYNC request.""" devices = [] for entity in hass.states.async_all(): @@ -354,7 +432,10 @@ def async_devices_query(hass, config, payload): # If we can't find a state, the device is offline devices[devid] = {'online': False} - devices[devid] = query_device(state, hass.config.units) + try: + devices[devid] = query_device(state, config, hass.config.units) + except SmartHomeError as error: + devices[devid] = {'errorCode': error.code} return {'devices': devices} diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index eb8d17a83aa41e773358ce8b0d423c86015b4fff..db075fb67890fe72a081b204f50a7d205b26eab3 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -230,4 +230,20 @@ DEMO_DEVICES = [{ 'traits': ['action.devices.traits.TemperatureSetting'], 'type': 'action.devices.types.THERMOSTAT', 'willReportState': False +}, { + 'id': 'sensor.outside_temperature', + 'name': { + 'name': 'Outside Temperature' + }, + 'traits': ['action.devices.traits.TemperatureSetting'], + 'type': 'action.devices.types.THERMOSTAT', + 'willReportState': False +}, { + 'id': 'sensor.outside_humidity', + 'name': { + 'name': 'Outside Humidity' + }, + 'traits': ['action.devices.traits.TemperatureSetting'], + 'type': 'action.devices.types.THERMOSTAT', + 'willReportState': False }] diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 500e8ece1439c4580823024eed19e16a83e08238..0d87b49122907c8e937d90e280bd681f0ed371e0 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -8,7 +8,7 @@ import pytest from homeassistant import core, const, setup from homeassistant.components import ( - fan, cover, light, switch, climate, async_setup, media_player) + fan, cover, light, switch, climate, async_setup, media_player, sensor) from homeassistant.components import google_assistant as ga from homeassistant.util.unit_system import IMPERIAL_SYSTEM @@ -43,6 +43,14 @@ def assistant_client(loop, hass, test_client): }, 'switch.decorative_lights': { 'type': 'light' + }, + 'sensor.outside_humidity': { + 'type': 'climate', + 'expose': True + }, + 'sensor.outside_temperature': { + 'type': 'climate', + 'expose': True } } } @@ -53,7 +61,7 @@ def assistant_client(loop, hass, test_client): @pytest.fixture def hass_fixture(loop, hass): - """Set up a HOme Assistant instance for these tests.""" + """Set up a Home Assistant instance for these tests.""" # We need to do this to get access to homeassistant/turn_(on,off) loop.run_until_complete(async_setup(hass, {core.DOMAIN: {}})) @@ -97,6 +105,13 @@ def hass_fixture(loop, hass): }] })) + loop.run_until_complete( + setup.async_setup_component(hass, sensor.DOMAIN, { + 'sensor': [{ + 'platform': 'demo' + }] + })) + return hass @@ -194,6 +209,8 @@ def test_query_climate_request(hass_fixture, assistant_client): {'id': 'climate.hvac'}, {'id': 'climate.heatpump'}, {'id': 'climate.ecobee'}, + {'id': 'sensor.outside_temperature'}, + {'id': 'sensor.outside_humidity'} ] } }] @@ -223,6 +240,12 @@ def test_query_climate_request(hass_fixture, assistant_client): 'thermostatTemperatureAmbient': 22, 'thermostatMode': 'cool', 'thermostatHumidityAmbient': 54, + }, + 'sensor.outside_temperature': { + 'thermostatTemperatureAmbient': 15.6 + }, + 'sensor.outside_humidity': { + 'thermostatHumidityAmbient': 54.0 } } @@ -242,6 +265,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client): {'id': 'climate.hvac'}, {'id': 'climate.heatpump'}, {'id': 'climate.ecobee'}, + {'id': 'sensor.outside_temperature'} ] } }] @@ -271,6 +295,9 @@ def test_query_climate_request_f(hass_fixture, assistant_client): 'thermostatTemperatureAmbient': -5.6, 'thermostatMode': 'cool', 'thermostatHumidityAmbient': 54, + }, + 'sensor.outside_temperature': { + 'thermostatTemperatureAmbient': -9.1 } }