diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index f580df9eab1..275c024667f 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -15,9 +15,7 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_HUMIDITY, - DEFAULT_MAX_TEMP, DEFAULT_MIN_HUMIDITY, - DEFAULT_MIN_TEMP, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -42,12 +40,14 @@ from homeassistant.const import ( PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util.unit_conversion import TemperatureConverter from . import subscription from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA @@ -150,6 +150,8 @@ CONF_TEMP_MAX = "max_temp" CONF_TEMP_MIN = "min_temp" CONF_TEMP_STEP = "temp_step" +DEFAULT_INITIAL_TEMPERATURE = 21.0 + MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( { climate.ATTR_AUX_HEAT, @@ -338,9 +340,9 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ): cv.ensure_list, vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_SWING_MODE_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_TEMP_INITIAL, default=21): cv.positive_int, - vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), - vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_TEMP_INITIAL): cv.positive_int, + vol.Optional(CONF_TEMP_MIN): vol.Coerce(float), + vol.Optional(CONF_TEMP_MAX): vol.Coerce(float), vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_COMMAND_TOPIC): valid_publish_topic, @@ -443,6 +445,9 @@ class MqttTemperatureControlEntity(MqttEntity, ABC): climate and water_heater platforms. """ + _attr_target_temperature_low: float | None + _attr_target_temperature_high: float | None + _optimistic: bool _topic: dict[str, Any] @@ -637,7 +642,7 @@ class MqttTemperatureControlEntity(MqttEntity, ABC): self.async_write_ha_state() -class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): # type: ignore[misc] +class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): """Representation of an MQTT climate device.""" _entity_id_format = climate.ENTITY_ID_FORMAT @@ -668,28 +673,41 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): # type: ignore[ def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._attr_hvac_modes = config[CONF_MODE_LIST] - self._attr_min_temp = config[CONF_TEMP_MIN] - self._attr_max_temp = config[CONF_TEMP_MAX] - self._attr_min_humidity = config[CONF_HUMIDITY_MIN] - self._attr_max_humidity = config[CONF_HUMIDITY_MAX] - self._attr_precision = config.get(CONF_PRECISION, super().precision) - self._attr_fan_modes = config[CONF_FAN_MODE_LIST] - self._attr_swing_modes = config[CONF_SWING_MODE_LIST] - self._attr_target_temperature_step = config[CONF_TEMP_STEP] + # Make sure the min an max temp is converted to the correct when not set self._attr_temperature_unit = config.get( CONF_TEMPERATURE_UNIT, self.hass.config.units.temperature_unit ) + if (min_temp := config.get(CONF_TEMP_MIN)) is not None: + self._attr_min_temp = min_temp + if (max_temp := config.get(CONF_TEMP_MAX)) is not None: + self._attr_max_temp = max_temp + self._attr_min_humidity = config[CONF_HUMIDITY_MIN] + self._attr_max_humidity = config[CONF_HUMIDITY_MAX] + if (precision := config.get(CONF_PRECISION)) is not None: + self._attr_precision = precision + self._attr_fan_modes = config[CONF_FAN_MODE_LIST] + self._attr_swing_modes = config[CONF_SWING_MODE_LIST] + self._attr_target_temperature_step = config[CONF_TEMP_STEP] self._topic = {key: config.get(key) for key in TOPIC_KEYS} self._optimistic = config[CONF_OPTIMISTIC] + # Set init temp, if it is missing convert the default to the temperature units + init_temp: float = config.get( + CONF_TEMP_INITIAL, + TemperatureConverter.convert( + DEFAULT_INITIAL_TEMPERATURE, + UnitOfTemperature.CELSIUS, + self.temperature_unit, + ), + ) if self._topic[CONF_TEMP_STATE_TOPIC] is None or self._optimistic: - self._attr_target_temperature = config[CONF_TEMP_INITIAL] + self._attr_target_temperature = init_temp if self._topic[CONF_TEMP_LOW_STATE_TOPIC] is None or self._optimistic: - self._attr_target_temperature_low = config[CONF_TEMP_INITIAL] + self._attr_target_temperature_low = init_temp if self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is None or self._optimistic: - self._attr_target_temperature_high = config[CONF_TEMP_INITIAL] + self._attr_target_temperature_high = init_temp if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None or self._optimistic: self._attr_fan_mode = FAN_LOW diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 384f03a317c..452a9b862ff 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -27,8 +27,11 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.components.mqtt.climate import MQTT_CLIMATE_ATTRIBUTES_BLOCKED -from homeassistant.const import ATTR_TEMPERATURE, Platform +from homeassistant.components.mqtt.climate import ( + DEFAULT_INITIAL_TEMPERATURE, + MQTT_CLIMATE_ATTRIBUTES_BLOCKED, +) +from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature from homeassistant.core import HomeAssistant from .test_common import ( @@ -1690,12 +1693,97 @@ async def test_temperature_unit( """Test that setting temperature unit converts temperature values.""" await mqtt_mock_entry() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == DEFAULT_INITIAL_TEMPERATURE + assert state.attributes.get("min_temp") == DEFAULT_MIN_TEMP + assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP + async_fire_mqtt_message(hass, "current_temperature", "77") state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("current_temperature") == 25 +@pytest.mark.parametrize( + ("hass_config", "temperature_unit", "initial", "min", "max", "current"), + [ + ( + help_custom_config( + climate.DOMAIN, + DEFAULT_CONFIG, + ( + { + "temperature_unit": "F", + "current_temperature_topic": "current_temperature", + }, + ), + ), + UnitOfTemperature.CELSIUS, + DEFAULT_INITIAL_TEMPERATURE, + DEFAULT_MIN_TEMP, + DEFAULT_MAX_TEMP, + 25, + ), + ( + help_custom_config( + climate.DOMAIN, + DEFAULT_CONFIG, + ( + { + "temperature_unit": "F", + "current_temperature_topic": "current_temperature", + }, + ), + ), + UnitOfTemperature.KELVIN, + 294, + 280, + 308, + 298, + ), + ( + help_custom_config( + climate.DOMAIN, + DEFAULT_CONFIG, + ( + { + "temperature_unit": "F", + "current_temperature_topic": "current_temperature", + }, + ), + ), + UnitOfTemperature.FAHRENHEIT, + 70, + 45, + 95, + 77, + ), + ], +) +async def test_alt_temperature_unit( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + temperature_unit: UnitOfTemperature, + initial: float, + min: float, + max: float, + current: float, +) -> None: + """Test deriving the systems temperature unit.""" + with patch.object(hass.config.units, "temperature_unit", temperature_unit): + await mqtt_mock_entry() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == initial + assert state.attributes.get("min_temp") == min + assert state.attributes.get("max_temp") == max + + async_fire_mqtt_message(hass, "current_temperature", "77") + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("current_temperature") == current + + async def test_setting_attribute_via_mqtt_json_message( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: