diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 25013dad171..7a1e1f9d941 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -88,6 +88,7 @@ TRAIT_BRIGHTNESS = f"{PREFIX_TRAITS}Brightness" TRAIT_COLOR_SETTING = f"{PREFIX_TRAITS}ColorSetting" TRAIT_SCENE = f"{PREFIX_TRAITS}Scene" TRAIT_TEMPERATURE_SETTING = f"{PREFIX_TRAITS}TemperatureSetting" +TRAIT_TEMPERATURE_CONTROL = f"{PREFIX_TRAITS}TemperatureControl" TRAIT_LOCKUNLOCK = f"{PREFIX_TRAITS}LockUnlock" TRAIT_FANSPEED = f"{PREFIX_TRAITS}FanSpeed" TRAIT_MODES = f"{PREFIX_TRAITS}Modes" @@ -683,6 +684,52 @@ class StartStopTrait(_Trait): ) +@register_trait +class TemperatureControlTrait(_Trait): + """Trait for devices (other than thermostats) that support controlling temperature. Workaround for Temperature sensors. + + https://developers.google.com/assistant/smarthome/traits/temperaturecontrol + """ + + name = TRAIT_TEMPERATURE_CONTROL + + @staticmethod + def supported(domain, features, device_class, _): + """Test if state is supported.""" + return ( + domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_TEMPERATURE + ) + + def sync_attributes(self): + """Return temperature attributes for a sync request.""" + return { + "temperatureUnitForUX": _google_temp_unit( + self.hass.config.units.temperature_unit + ), + "queryOnlyTemperatureSetting": True, + "temperatureRange": { + "minThresholdCelsius": -100, + "maxThresholdCelsius": 100, + }, + } + + def query_attributes(self): + """Return temperature states.""" + response = {} + unit = self.hass.config.units.temperature_unit + current_temp = self.state.state + if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + temp = round(temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1) + response["temperatureSetpointCelsius"] = temp + response["temperatureAmbientCelsius"] = temp + + return response + + async def execute(self, command, data, params, challenge): + """Unsupported.""" + raise SmartHomeError(ERR_NOT_SUPPORTED, "Execute is not supported by sensor") + + @register_trait class TemperatureSettingTrait(_Trait): """Trait to offer handling both temperature point and modes functionality. @@ -715,12 +762,7 @@ class TemperatureSettingTrait(_Trait): @staticmethod def supported(domain, features, device_class, _): """Test if state is supported.""" - if domain == climate.DOMAIN: - return True - - return ( - domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_TEMPERATURE - ) + return domain == climate.DOMAIN @property def climate_google_modes(self): @@ -743,32 +785,24 @@ class TemperatureSettingTrait(_Trait): def sync_attributes(self): """Return temperature point and modes attributes for a sync request.""" response = {} - attrs = self.state.attributes - domain = self.state.domain response["thermostatTemperatureUnit"] = _google_temp_unit( self.hass.config.units.temperature_unit ) - if domain == sensor.DOMAIN: - device_class = attrs.get(ATTR_DEVICE_CLASS) - if device_class == sensor.DEVICE_CLASS_TEMPERATURE: - response["queryOnlyTemperatureSetting"] = True + modes = self.climate_google_modes - elif domain == climate.DOMAIN: - modes = self.climate_google_modes + # Some integrations don't support modes (e.g. opentherm), but Google doesn't + # support changing the temperature if we don't have any modes. If there's + # only one Google doesn't support changing it, so the default mode here is + # only cosmetic. + if len(modes) == 0: + modes.append("heat") - # Some integrations don't support modes (e.g. opentherm), but Google doesn't - # support changing the temperature if we don't have any modes. If there's - # only one Google doesn't support changing it, so the default mode here is - # only cosmetic. - if len(modes) == 0: - modes.append("heat") - - if "off" in modes and any( - mode in modes for mode in ("heatcool", "heat", "cool") - ): - modes.append("on") - response["availableThermostatModes"] = modes + if "off" in modes and any( + mode in modes for mode in ("heatcool", "heat", "cool") + ): + modes.append("on") + response["availableThermostatModes"] = modes return response @@ -776,76 +810,60 @@ class TemperatureSettingTrait(_Trait): """Return temperature point and modes query attributes.""" response = {} attrs = self.state.attributes - domain = self.state.domain unit = self.hass.config.units.temperature_unit - if domain == sensor.DOMAIN: - device_class = attrs.get(ATTR_DEVICE_CLASS) - if device_class == sensor.DEVICE_CLASS_TEMPERATURE: - current_temp = self.state.state - if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - response["thermostatTemperatureAmbient"] = round( - temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1 - ) - elif domain == climate.DOMAIN: - operation = self.state.state - preset = attrs.get(climate.ATTR_PRESET_MODE) - supported = attrs.get(ATTR_SUPPORTED_FEATURES, 0) + operation = self.state.state + preset = attrs.get(climate.ATTR_PRESET_MODE) + supported = attrs.get(ATTR_SUPPORTED_FEATURES, 0) - if preset in self.preset_to_google: - response["thermostatMode"] = self.preset_to_google[preset] - else: - response["thermostatMode"] = self.hvac_to_google.get(operation) + if preset in self.preset_to_google: + response["thermostatMode"] = self.preset_to_google[preset] + else: + response["thermostatMode"] = self.hvac_to_google.get(operation) - current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) - if current_temp is not None: - response["thermostatTemperatureAmbient"] = round( - temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1 + current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) + if current_temp is not None: + response["thermostatTemperatureAmbient"] = round( + temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1 + ) + + current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) + if current_humidity is not None: + response["thermostatHumidityAmbient"] = current_humidity + + if operation in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): + if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: + response["thermostatTemperatureSetpointHigh"] = round( + temp_util.convert( + attrs[climate.ATTR_TARGET_TEMP_HIGH], unit, TEMP_CELSIUS + ), + 1, + ) + response["thermostatTemperatureSetpointLow"] = round( + temp_util.convert( + attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS + ), + 1, ) - - current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) - if current_humidity is not None: - response["thermostatHumidityAmbient"] = current_humidity - - if operation in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): - if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: - response["thermostatTemperatureSetpointHigh"] = round( - temp_util.convert( - attrs[climate.ATTR_TARGET_TEMP_HIGH], unit, TEMP_CELSIUS - ), - 1, - ) - response["thermostatTemperatureSetpointLow"] = round( - temp_util.convert( - attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS - ), - 1, - ) - else: - target_temp = attrs.get(ATTR_TEMPERATURE) - if target_temp is not None: - target_temp = round( - temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 - ) - response["thermostatTemperatureSetpointHigh"] = target_temp - response["thermostatTemperatureSetpointLow"] = target_temp else: target_temp = attrs.get(ATTR_TEMPERATURE) if target_temp is not None: - response["thermostatTemperatureSetpoint"] = round( + target_temp = round( temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 ) + response["thermostatTemperatureSetpointHigh"] = target_temp + response["thermostatTemperatureSetpointLow"] = target_temp + else: + target_temp = attrs.get(ATTR_TEMPERATURE) + if target_temp is not None: + response["thermostatTemperatureSetpoint"] = round( + temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 + ) return response async def execute(self, command, data, params, challenge): """Execute a temperature point or mode command.""" - domain = self.state.domain - if domain == sensor.DOMAIN: - raise SmartHomeError( - ERR_NOT_SUPPORTED, "Execute is not supported by sensor" - ) - # All sent in temperatures are always in Celsius unit = self.hass.config.units.temperature_unit min_temp = self.state.attributes[climate.ATTR_MIN_TEMP] diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 1d70027024a..3d506be644d 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -954,6 +954,29 @@ async def test_temperature_setting_climate_setpoint_auto(hass): assert calls[0].data == {ATTR_ENTITY_ID: "climate.bla", ATTR_TEMPERATURE: 19} +async def test_temperature_control(hass): + """Test TemperatureControl trait support for sensor domain.""" + hass.config.units.temperature_unit = TEMP_CELSIUS + + trt = trait.TemperatureControlTrait( + hass, + State("sensor.temp", 18), + BASIC_CONFIG, + ) + assert trt.sync_attributes() == { + "queryOnlyTemperatureSetting": True, + "temperatureUnitForUX": "C", + "temperatureRange": {"maxThresholdCelsius": 100, "minThresholdCelsius": -100}, + } + assert trt.query_attributes() == { + "temperatureSetpointCelsius": 18, + "temperatureAmbientCelsius": 18, + } + with pytest.raises(helpers.SmartHomeError) as err: + await trt.execute(trait.COMMAND_ONOFF, BASIC_DATA, {"on": False}, {}) + assert err.value.code == const.ERR_NOT_SUPPORTED + + async def test_humidity_setting_humidifier_setpoint(hass): """Test HumiditySetting trait support for humidifier domain - setpoint.""" assert helpers.get_google_type(humidifier.DOMAIN, None) is not None @@ -2380,16 +2403,16 @@ async def test_media_player_mute(hass): } -async def test_temperature_setting_sensor(hass): - """Test TemperatureSetting trait support for temperature sensor.""" +async def test_temperature_control_sensor(hass): + """Test TemperatureControl trait support for temperature sensor.""" assert ( helpers.get_google_type(sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE) is not None ) - assert not trait.TemperatureSettingTrait.supported( + assert not trait.TemperatureControlTrait.supported( sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY, None ) - assert trait.TemperatureSettingTrait.supported( + assert trait.TemperatureControlTrait.supported( sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE, None ) @@ -2403,11 +2426,11 @@ async def test_temperature_setting_sensor(hass): (TEMP_FAHRENHEIT, "F", "unknown", None), ], ) -async def test_temperature_setting_sensor_data(hass, unit_in, unit_out, state, ambient): - """Test TemperatureSetting trait support for temperature sensor.""" +async def test_temperature_control_sensor_data(hass, unit_in, unit_out, state, ambient): + """Test TemperatureControl trait support for temperature sensor.""" hass.config.units.temperature_unit = unit_in - trt = trait.TemperatureSettingTrait( + trt = trait.TemperatureControlTrait( hass, State( "sensor.test", state, {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TEMPERATURE} @@ -2417,11 +2440,15 @@ async def test_temperature_setting_sensor_data(hass, unit_in, unit_out, state, a assert trt.sync_attributes() == { "queryOnlyTemperatureSetting": True, - "thermostatTemperatureUnit": unit_out, + "temperatureUnitForUX": unit_out, + "temperatureRange": {"maxThresholdCelsius": 100, "minThresholdCelsius": -100}, } if ambient: - assert trt.query_attributes() == {"thermostatTemperatureAmbient": ambient} + assert trt.query_attributes() == { + "temperatureAmbientCelsius": ambient, + "temperatureSetpointCelsius": ambient, + } else: assert trt.query_attributes() == {} hass.config.units.temperature_unit = TEMP_CELSIUS