diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 64a5908368b..5b38a34e85e 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -31,6 +31,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, + CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_TEMPERATURE_UNIT, @@ -47,7 +48,13 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA -from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, PAYLOAD_NONE +from .const import ( + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + DEFAULT_OPTIMISTIC, + PAYLOAD_NONE, +) from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, @@ -242,6 +249,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic, @@ -364,6 +372,7 @@ class MqttClimate(MqttEntity, ClimateEntity): _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]] _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] _feature_preset_mode: bool + _optimistic: bool _optimistic_preset_mode: bool _topic: dict[str, Any] @@ -405,6 +414,8 @@ class MqttClimate(MqttEntity, ClimateEntity): self._attr_target_temperature_low = None self._attr_target_temperature_high = None + self._optimistic = config[CONF_OPTIMISTIC] + if self._topic[CONF_TEMP_STATE_TOPIC] is None: self._attr_target_temperature = config[CONF_TEMP_INITIAL] if self._topic[CONF_TEMP_LOW_STATE_TOPIC] is None: @@ -428,7 +439,9 @@ class MqttClimate(MqttEntity, ClimateEntity): self._attr_preset_mode = PRESET_NONE else: self._attr_preset_modes = [] - self._optimistic_preset_mode = CONF_PRESET_MODE_STATE_TOPIC not in config + self._optimistic_preset_mode = ( + self._optimistic or CONF_PRESET_MODE_STATE_TOPIC not in config + ) self._attr_hvac_action = None self._attr_is_aux_heat = False @@ -738,14 +751,18 @@ class MqttClimate(MqttEntity, ClimateEntity): cmnd_template: str, state_topic: str, attr: str, - ) -> None: - if temp is not None: - if self._topic[state_topic] is None: - # optimistic mode - setattr(self, attr, temp) + ) -> bool: + if temp is None: + return False + changed = False + if self._optimistic or self._topic[state_topic] is None: + # optimistic mode + changed = True + setattr(self, attr, temp) - payload = self._command_templates[cmnd_template](temp) - await self._publish(cmnd_topic, payload) + payload = self._command_templates[cmnd_template](temp) + await self._publish(cmnd_topic, payload) + return changed async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" @@ -753,7 +770,7 @@ class MqttClimate(MqttEntity, ClimateEntity): if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: await self.async_set_hvac_mode(operation_mode) - await self._set_temperature( + changed = await self._set_temperature( kwargs.get(ATTR_TEMPERATURE), CONF_TEMP_COMMAND_TOPIC, CONF_TEMP_COMMAND_TEMPLATE, @@ -761,7 +778,7 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature", ) - await self._set_temperature( + changed |= await self._set_temperature( kwargs.get(ATTR_TARGET_TEMP_LOW), CONF_TEMP_LOW_COMMAND_TOPIC, CONF_TEMP_LOW_COMMAND_TEMPLATE, @@ -769,7 +786,7 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature_low", ) - await self._set_temperature( + changed |= await self._set_temperature( kwargs.get(ATTR_TARGET_TEMP_HIGH), CONF_TEMP_HIGH_COMMAND_TOPIC, CONF_TEMP_HIGH_COMMAND_TEMPLATE, @@ -777,6 +794,8 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature_high", ) + if not changed: + return self.async_write_ha_state() async def async_set_swing_mode(self, swing_mode: str) -> None: @@ -784,7 +803,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](swing_mode) await self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) - if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: + if self._optimistic or self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: self._attr_swing_mode = swing_mode self.async_write_ha_state() @@ -793,7 +812,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) - if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: + if self._optimistic or self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: self._attr_fan_mode = fan_mode self.async_write_ha_state() @@ -809,7 +828,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[CONF_MODE_COMMAND_TEMPLATE](hvac_mode) await self._publish(CONF_MODE_COMMAND_TOPIC, payload) - if self._topic[CONF_MODE_STATE_TOPIC] is None: + if self._optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None: self._attr_hvac_mode = hvac_mode self.async_write_ha_state() @@ -839,7 +858,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], ) - if self._topic[CONF_AUX_STATE_TOPIC] is None: + if self._optimistic or self._topic[CONF_AUX_STATE_TOPIC] is None: self._attr_is_aux_heat = state self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py index 88adcac7194..895a8e3b581 100644 --- a/homeassistant/components/mqtt/config.py +++ b/homeassistant/components/mqtt/config.py @@ -3,7 +3,7 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE from homeassistant.helpers import config_validation as cv from .const import ( @@ -13,6 +13,7 @@ from .const import ( CONF_RETAIN, CONF_STATE_TOPIC, DEFAULT_ENCODING, + DEFAULT_OPTIMISTIC, DEFAULT_QOS, DEFAULT_RETAIN, ) @@ -37,6 +38,7 @@ MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, } diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 0cb81dc9f74..ddcf47f9148 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -41,6 +41,7 @@ DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" DEFAULT_DISCOVERY = True DEFAULT_ENCODING = "utf-8" +DEFAULT_OPTIMISTIC = False DEFAULT_QOS = 0 DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 9d5f58eaa6d..4770e6e57c9 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -42,6 +42,7 @@ from .const import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + DEFAULT_OPTIMISTIC, ) from .debug_info import log_messages from .mixins import ( @@ -84,7 +85,6 @@ TILT_PAYLOAD = "tilt" COVER_PAYLOAD = "cover" DEFAULT_NAME = "MQTT Cover" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_CLOSE = "CLOSE" DEFAULT_PAYLOAD_OPEN = "OPEN" DEFAULT_PAYLOAD_STOP = "STOP" diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 82fd0d4063a..61fbc4fd387 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -88,7 +88,6 @@ DEFAULT_NAME = "MQTT Fan" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_RESET = "None" -DEFAULT_OPTIMISTIC = False DEFAULT_SPEED_RANGE_MIN = 1 DEFAULT_SPEED_RANGE_MAX = 100 @@ -128,7 +127,6 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 38d3e46ea45..ab51b0b9b45 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -76,7 +76,6 @@ CONF_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" CONF_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" DEFAULT_NAME = "MQTT Humidifier" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_RESET = "None" @@ -128,7 +127,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index ab5d28d8a68..41f4c15af15 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -136,7 +136,6 @@ MQTT_LIGHT_ATTRIBUTES_BLOCKED = frozenset( DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_NAME = "MQTT LightEntity" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_WHITE_SCALE = 255 @@ -195,7 +194,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In( VALUES_ON_COMMAND_TYPE ), - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_RGB_COMMAND_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 09413e1f0ac..0ba523c73f6 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -83,7 +83,6 @@ DEFAULT_EFFECT = False DEFAULT_FLASH_TIME_LONG = 10 DEFAULT_FLASH_TIME_SHORT = 2 DEFAULT_NAME = "MQTT JSON Light" -DEFAULT_OPTIMISTIC = False DEFAULT_RGB = False DEFAULT_XY = False DEFAULT_HS = False @@ -135,7 +134,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) ), diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 21691acc916..654ca205a65 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -63,7 +63,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "mqtt_template" DEFAULT_NAME = "MQTT Template Light" -DEFAULT_OPTIMISTIC = False CONF_BLUE_TEMPLATE = "blue_template" CONF_BRIGHTNESS_TEMPLATE = "brightness_template" @@ -103,7 +102,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_RED_TEMPLATE): cv.template, vol.Optional(CONF_STATE_TEMPLATE): cv.template, } diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index b956f2e1b88..a518300b7f0 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -43,7 +43,6 @@ CONF_STATE_LOCKED = "state_locked" CONF_STATE_UNLOCKED = "state_unlocked" DEFAULT_NAME = "MQTT Lock" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" DEFAULT_PAYLOAD_OPEN = "OPEN" @@ -60,7 +59,6 @@ MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_PAYLOAD_OPEN): cv.string, diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 8f33eea2c64..1cd37342d75 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -64,7 +64,6 @@ CONF_MAX = "max" CONF_STEP = "step" DEFAULT_NAME = "MQTT Number" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_RESET = "None" MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset( @@ -92,7 +91,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_RESET, default=DEFAULT_PAYLOAD_RESET): cv.string, vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( vol.Coerce(float), vol.Range(min=1e-3) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index d574cf081ba..65cf406522c 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -48,7 +48,6 @@ _LOGGER = logging.getLogger(__name__) CONF_OPTIONS = "options" DEFAULT_NAME = "MQTT Select" -DEFAULT_OPTIMISTIC = False MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset( { @@ -61,7 +60,6 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Required(CONF_OPTIONS): cv.ensure_list, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 6043773d5d6..13ccfdc9cb2 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -67,7 +67,6 @@ from .util import get_mqtt_data DEFAULT_NAME = "MQTT Siren" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" -DEFAULT_OPTIMISTIC = False ENTITY_ID_FORMAT = siren.DOMAIN + ".{}" @@ -86,7 +85,6 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_OFF_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_OFF): cv.string, diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 09e72955e63..800c8e7dd91 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -49,14 +49,12 @@ from .util import get_mqtt_data DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" -DEFAULT_OPTIMISTIC = False CONF_STATE_ON = "state_on" CONF_STATE_OFF = "state_off" PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_OFF): cv.string, diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index 032dba66719..824aeb2f4c5 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -52,7 +52,6 @@ CONF_MIN = "min" CONF_PATTERN = "pattern" DEFAULT_NAME = "MQTT Text" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_RESET = "None" MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset( @@ -84,7 +83,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_MODE, default=text.TextMode.TEXT): vol.In( [text.TextMode.TEXT, text.TextMode.PASSWORD] ), - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PATTERN): cv.is_regex, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 16872c0c49d..227b725bd00 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -240,6 +240,31 @@ async def test_set_operation_pessimistic(hass, mqtt_mock_entry_with_yaml_config) assert state.state == "cool" +async def test_set_operation_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting operation mode in optimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["mode_state_topic"] = "mode-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "unknown" + + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "cool" + + async_fire_mqtt_message(hass, "mode-state", "heat") + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "heat" + + async_fire_mqtt_message(hass, "mode-state", "bogus mode") + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "heat" + + async def test_set_operation_with_power_command(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode with power command enabled.""" config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) @@ -308,6 +333,31 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("fan_mode") == "high" +async def test_set_fan_mode_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting of new fan mode in optimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["fan_mode_state_topic"] = "fan-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") is None + + await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "high" + + async_fire_mqtt_message(hass, "fan-state", "low") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "low" + + async_fire_mqtt_message(hass, "fan-state", "bogus mode") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "low" + + async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode.""" assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) @@ -363,6 +413,31 @@ async def test_set_swing_pessimistic(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("swing_mode") == "on" +async def test_set_swing_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting swing mode in optimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["swing_mode_state_topic"] = "swing-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") is None + + await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "on" + + async_fire_mqtt_message(hass, "swing-state", "off") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "off" + + async_fire_mqtt_message(hass, "swing-state", "bogus state") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "off" + + async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new swing mode.""" assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) @@ -440,6 +515,33 @@ async def test_set_target_temperature_pessimistic( assert state.attributes.get("temperature") == 1701 +async def test_set_target_temperature_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): + """Test setting the target temperature optimistic.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["temperature_state_topic"] = "temperature-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") is None + await common.async_set_hvac_mode(hass, "heat", ENTITY_CLIMATE) + await common.async_set_temperature(hass, temperature=17, entity_id=ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 17 + + async_fire_mqtt_message(hass, "temperature-state", "18") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 18 + + async_fire_mqtt_message(hass, "temperature-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 18 + + async def test_set_target_temperature_low_high(hass, mqtt_mock_entry_with_yaml_config): """Test setting the low/high target temperature.""" assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) @@ -496,6 +598,47 @@ async def test_set_target_temperature_low_highpessimistic( assert state.attributes.get("target_temp_high") == 1703 +async def test_set_target_temperature_low_high_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): + """Test setting the low/high target temperature optimistic.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["optimistic"] = True + config["climate"]["temperature_low_state_topic"] = "temperature-low-state" + config["climate"]["temperature_high_state_topic"] = "temperature-high-state" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") is None + assert state.attributes.get("target_temp_high") is None + await common.async_set_temperature( + hass, target_temp_low=20, target_temp_high=23, entity_id=ENTITY_CLIMATE + ) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 20 + assert state.attributes.get("target_temp_high") == 23 + + async_fire_mqtt_message(hass, "temperature-low-state", "15") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 15 + assert state.attributes.get("target_temp_high") == 23 + + async_fire_mqtt_message(hass, "temperature-high-state", "25") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 15 + assert state.attributes.get("target_temp_high") == 25 + + async_fire_mqtt_message(hass, "temperature-low-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 15 + + async_fire_mqtt_message(hass, "temperature-high-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_high") == 25 + + async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test getting the current temperature via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) @@ -580,6 +723,56 @@ async def test_set_preset_mode_optimistic( assert "'invalid' is not a valid preset mode" in caplog.text +async def test_set_preset_mode_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): + """Test setting of the preset mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["optimistic"] = True + config["climate"]["preset_mode_state_topic"] = "preset-mode-state" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "none" + + await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "away", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "away" + + await common.async_set_preset_mode(hass, "eco", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "eco", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "eco" + + await common.async_set_preset_mode(hass, "none", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "none", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "none" + + await common.async_set_preset_mode(hass, "comfort", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "preset-mode-topic", "comfort", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "comfort" + + await common.async_set_preset_mode(hass, "invalid", ENTITY_CLIMATE) + assert "'invalid' is not a valid preset mode" in caplog.text + + async def test_set_preset_mode_pessimistic( hass, mqtt_mock_entry_with_yaml_config, caplog ):