Add optimistic option for MQTT climate (#84777)

This commit is contained in:
Jan Bouwhuis 2023-01-02 11:39:42 +01:00 committed by GitHub
parent bcbae1388d
commit 9cf86b234b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 233 additions and 40 deletions

View file

@ -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()

View file

@ -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,
}

View file

@ -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"

View file

@ -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"

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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])
),

View file

@ -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,
}

View file

@ -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,

View file

@ -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)

View file

@ -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,
},

View file

@ -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,

View file

@ -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,

View file

@ -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,
},

View file

@ -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
):