mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Allow exposing sensors as temperature or humidity 'climate' devices to Google Assistant (#11095)
* Allow exposing sensors as temperature or humidity as 'climate' to Google Assistant * Fixed hound * Fixed hound * Handled correctly unit of measurement to fix humidity * Fixed temperature conversion for Google climate components * Fixed temperature conversion for Google climate components * Fixed indentation * Fixed hound * Fixed tests * Fixed conversion and unit tests * Fix sync for custom unit temperature * Implemented requested changes * Fix hound * Fix linting errors * Added success tests for sensors as climate * Fix lint errors
This commit is contained in:
parent
183e0543b4
commit
bc13c9db83
@ -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}
|
||||
|
||||
|
@ -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
|
||||
}]
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user