mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +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.util.decorator import Registry
|
||||||
|
|
||||||
from homeassistant.const import (
|
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,
|
STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||||
CONF_NAME, CONF_TYPE
|
CONF_NAME, CONF_TYPE
|
||||||
)
|
)
|
||||||
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,
|
||||||
|
sensor
|
||||||
)
|
)
|
||||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||||
|
|
||||||
@ -67,6 +68,23 @@ MAPPING_COMPONENT = {
|
|||||||
} # type: Dict[str, list]
|
} # 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:
|
class Config:
|
||||||
"""Hold the configuration for Google Assistant."""
|
"""Hold the configuration for Google Assistant."""
|
||||||
|
|
||||||
@ -80,8 +98,9 @@ class Config:
|
|||||||
def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
|
def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
|
||||||
"""Convert a hass entity into an google actions device."""
|
"""Convert a hass entity into an google actions device."""
|
||||||
entity_config = config.entity_config.get(entity.entity_id, {})
|
entity_config = config.entity_config.get(entity.entity_id, {})
|
||||||
|
google_domain = entity_config.get(CONF_TYPE)
|
||||||
class_data = MAPPING_COMPONENT.get(
|
class_data = MAPPING_COMPONENT.get(
|
||||||
entity_config.get(CONF_TYPE) or entity.domain)
|
google_domain or entity.domain)
|
||||||
|
|
||||||
if class_data is None:
|
if class_data is None:
|
||||||
return 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',
|
'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C',
|
||||||
}
|
}
|
||||||
_LOGGER.debug('Thermostat attributes %s', device['attributes'])
|
_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
|
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."""
|
"""Take an entity and return a properly formatted device object."""
|
||||||
def celsius(deg: Optional[float]) -> Optional[float]:
|
def celsius(deg: Optional[float]) -> Optional[float]:
|
||||||
"""Convert a float to Celsius and rounds to one decimal place."""
|
"""Convert a float to Celsius and rounds to one decimal place."""
|
||||||
if deg is None:
|
if deg is None:
|
||||||
return None
|
return None
|
||||||
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
|
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:
|
if entity.domain == climate.DOMAIN:
|
||||||
mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower()
|
mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower()
|
||||||
if mode not in CLIMATE_SUPPORTED_MODES:
|
if mode not in CLIMATE_SUPPORTED_MODES:
|
||||||
@ -317,7 +395,7 @@ def async_handle_message(hass, config, message):
|
|||||||
|
|
||||||
@HANDLERS.register('action.devices.SYNC')
|
@HANDLERS.register('action.devices.SYNC')
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_devices_sync(hass, config, payload):
|
def async_devices_sync(hass, config: Config, payload):
|
||||||
"""Handle action.devices.SYNC request."""
|
"""Handle action.devices.SYNC request."""
|
||||||
devices = []
|
devices = []
|
||||||
for entity in hass.states.async_all():
|
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
|
# If we can't find a state, the device is offline
|
||||||
devices[devid] = {'online': False}
|
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}
|
return {'devices': devices}
|
||||||
|
|
||||||
|
@ -230,4 +230,20 @@ DEMO_DEVICES = [{
|
|||||||
'traits': ['action.devices.traits.TemperatureSetting'],
|
'traits': ['action.devices.traits.TemperatureSetting'],
|
||||||
'type': 'action.devices.types.THERMOSTAT',
|
'type': 'action.devices.types.THERMOSTAT',
|
||||||
'willReportState': False
|
'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 import core, const, setup
|
||||||
from homeassistant.components import (
|
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.components import google_assistant as ga
|
||||||
from homeassistant.util.unit_system import IMPERIAL_SYSTEM
|
from homeassistant.util.unit_system import IMPERIAL_SYSTEM
|
||||||
|
|
||||||
@ -43,6 +43,14 @@ def assistant_client(loop, hass, test_client):
|
|||||||
},
|
},
|
||||||
'switch.decorative_lights': {
|
'switch.decorative_lights': {
|
||||||
'type': 'light'
|
'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
|
@pytest.fixture
|
||||||
def hass_fixture(loop, hass):
|
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)
|
# We need to do this to get access to homeassistant/turn_(on,off)
|
||||||
loop.run_until_complete(async_setup(hass, {core.DOMAIN: {}}))
|
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
|
return hass
|
||||||
|
|
||||||
|
|
||||||
@ -194,6 +209,8 @@ def test_query_climate_request(hass_fixture, assistant_client):
|
|||||||
{'id': 'climate.hvac'},
|
{'id': 'climate.hvac'},
|
||||||
{'id': 'climate.heatpump'},
|
{'id': 'climate.heatpump'},
|
||||||
{'id': 'climate.ecobee'},
|
{'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,
|
'thermostatTemperatureAmbient': 22,
|
||||||
'thermostatMode': 'cool',
|
'thermostatMode': 'cool',
|
||||||
'thermostatHumidityAmbient': 54,
|
'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.hvac'},
|
||||||
{'id': 'climate.heatpump'},
|
{'id': 'climate.heatpump'},
|
||||||
{'id': 'climate.ecobee'},
|
{'id': 'climate.ecobee'},
|
||||||
|
{'id': 'sensor.outside_temperature'}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
@ -271,6 +295,9 @@ def test_query_climate_request_f(hass_fixture, assistant_client):
|
|||||||
'thermostatTemperatureAmbient': -5.6,
|
'thermostatTemperatureAmbient': -5.6,
|
||||||
'thermostatMode': 'cool',
|
'thermostatMode': 'cool',
|
||||||
'thermostatHumidityAmbient': 54,
|
'thermostatHumidityAmbient': 54,
|
||||||
|
},
|
||||||
|
'sensor.outside_temperature': {
|
||||||
|
'thermostatTemperatureAmbient': -9.1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user