mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add MQTT climate setting for current humidity (#84592)
* MQTT Climate: Add support for setting the current humidity via MQTT * MQTT Climate: Add configuration constants related to setting the target humidity * MQTT Climate: Add support for setting the humidity's state topic & template * MQTT Climate: Add support for setting the initial humidity * MQTT Climate: Add support for setting the humidity's command topic & template * MQTT Climate: Add support for setting the min/max humidity * MQTT Climate: Fix style & tests * MQTT Climate: Set the initial humidity to None * MQTT Climate: Rename _set_mqtt_attribute to _set_climate_attribute and handle_temperature_received to handle_climate_attribute_received * MQTT Climate: Copy humidity range validation from MQTT Humidifier * MQTT Climate: Remove CONF_HUMIDITY_INITIAL * MQTT Climate: Only enable support for TARGET_HUMIDITY when the command topic is set * MQTT Climate: Check if setting the target humidity is supported before actually setting it * MQTT Climate: Make sure that CONF_HUMIDITY_COMMAND_TOPIC has been configured when setting CONF_HUMIDITY_STATE_TOPIC * MQTT Climate: Fix broken tests * MQTT Climate: Add test for optimistically setting the target humidity * MQTT Climate: Remove references to "temperature" in handle_climate_attribute_received * MQTT Climate: Add additional humidity-related tests * MQTT Climate: Remove supported feature check in handle_target_humidity_received It's not needed because this is covered by the `valid_humidity_state_configuration` validation. Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> * MQTT Climate: Remove supported feature check in async_set_humidity It is covered by the base Climate entity. Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
This commit is contained in:
parent
7fdf00a9fb
commit
799d527fb5
@ -43,6 +43,8 @@ ABBREVIATIONS = {
|
||||
"cod_arm_req": "code_arm_required",
|
||||
"cod_dis_req": "code_disarm_required",
|
||||
"cod_trig_req": "code_trigger_required",
|
||||
"curr_hum_t": "current_humidity_topic",
|
||||
"curr_hum_tpl": "current_humidity_template",
|
||||
"curr_temp_t": "current_temperature_topic",
|
||||
"curr_temp_tpl": "current_temperature_template",
|
||||
"dev": "device",
|
||||
|
@ -13,7 +13,9 @@ from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
DEFAULT_MAX_HUMIDITY,
|
||||
DEFAULT_MAX_TEMP,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
DEFAULT_MIN_TEMP,
|
||||
FAN_AUTO,
|
||||
FAN_HIGH,
|
||||
@ -85,6 +87,8 @@ CONF_AWAY_MODE_COMMAND_TOPIC = "away_mode_command_topic"
|
||||
CONF_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template"
|
||||
CONF_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic"
|
||||
|
||||
CONF_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template"
|
||||
CONF_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic"
|
||||
CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template"
|
||||
CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic"
|
||||
CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template"
|
||||
@ -99,6 +103,12 @@ CONF_HOLD_STATE_TEMPLATE = "hold_state_template"
|
||||
CONF_HOLD_STATE_TOPIC = "hold_state_topic"
|
||||
CONF_HOLD_LIST = "hold_modes"
|
||||
|
||||
CONF_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template"
|
||||
CONF_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic"
|
||||
CONF_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template"
|
||||
CONF_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic"
|
||||
CONF_HUMIDITY_MAX = "max_humidity"
|
||||
CONF_HUMIDITY_MIN = "min_humidity"
|
||||
CONF_MODE_COMMAND_TEMPLATE = "mode_command_template"
|
||||
CONF_MODE_COMMAND_TOPIC = "mode_command_topic"
|
||||
CONF_MODE_LIST = "modes"
|
||||
@ -164,8 +174,10 @@ MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset(
|
||||
|
||||
VALUE_TEMPLATE_KEYS = (
|
||||
CONF_AUX_STATE_TEMPLATE,
|
||||
CONF_CURRENT_HUMIDITY_TEMPLATE,
|
||||
CONF_CURRENT_TEMP_TEMPLATE,
|
||||
CONF_FAN_MODE_STATE_TEMPLATE,
|
||||
CONF_HUMIDITY_STATE_TEMPLATE,
|
||||
CONF_MODE_STATE_TEMPLATE,
|
||||
CONF_POWER_STATE_TEMPLATE,
|
||||
CONF_ACTION_TEMPLATE,
|
||||
@ -178,6 +190,7 @@ VALUE_TEMPLATE_KEYS = (
|
||||
|
||||
COMMAND_TEMPLATE_KEYS = {
|
||||
CONF_FAN_MODE_COMMAND_TEMPLATE,
|
||||
CONF_HUMIDITY_COMMAND_TEMPLATE,
|
||||
CONF_MODE_COMMAND_TEMPLATE,
|
||||
CONF_PRESET_MODE_COMMAND_TEMPLATE,
|
||||
CONF_SWING_MODE_COMMAND_TEMPLATE,
|
||||
@ -191,9 +204,12 @@ TOPIC_KEYS = (
|
||||
CONF_ACTION_TOPIC,
|
||||
CONF_AUX_COMMAND_TOPIC,
|
||||
CONF_AUX_STATE_TOPIC,
|
||||
CONF_CURRENT_HUMIDITY_TOPIC,
|
||||
CONF_CURRENT_TEMP_TOPIC,
|
||||
CONF_FAN_MODE_COMMAND_TOPIC,
|
||||
CONF_FAN_MODE_STATE_TOPIC,
|
||||
CONF_HUMIDITY_COMMAND_TOPIC,
|
||||
CONF_HUMIDITY_STATE_TOPIC,
|
||||
CONF_MODE_COMMAND_TOPIC,
|
||||
CONF_MODE_STATE_TOPIC,
|
||||
CONF_POWER_COMMAND_TOPIC,
|
||||
@ -218,11 +234,36 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def valid_humidity_range_configuration(config: ConfigType) -> ConfigType:
|
||||
"""Validate that the target_humidity range configuration is valid, throws if it isn't."""
|
||||
if config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]:
|
||||
raise ValueError("target_humidity_max must be > target_humidity_min")
|
||||
if config[CONF_HUMIDITY_MAX] > 100:
|
||||
raise ValueError("max_humidity must be <= 100")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def valid_humidity_state_configuration(config: ConfigType) -> ConfigType:
|
||||
"""Validate that if CONF_HUMIDITY_STATE_TOPIC is set then CONF_HUMIDITY_COMMAND_TOPIC is also set."""
|
||||
if (
|
||||
CONF_HUMIDITY_STATE_TOPIC in config
|
||||
and CONF_HUMIDITY_COMMAND_TOPIC not in config
|
||||
):
|
||||
raise ValueError(
|
||||
f"{CONF_HUMIDITY_STATE_TOPIC} cannot be used without {CONF_HUMIDITY_COMMAND_TOPIC}"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_CURRENT_HUMIDITY_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_CURRENT_HUMIDITY_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_CURRENT_TEMP_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template,
|
||||
@ -233,6 +274,16 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
|
||||
): cv.ensure_list,
|
||||
vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FAN_MODE_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_HUMIDITY_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_HUMIDITY_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY): vol.Coerce(
|
||||
float
|
||||
),
|
||||
vol.Optional(CONF_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY): vol.Coerce(
|
||||
float
|
||||
),
|
||||
vol.Optional(CONF_HUMIDITY_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_HUMIDITY_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(
|
||||
@ -313,6 +364,8 @@ PLATFORM_SCHEMA_MODERN = vol.All(
|
||||
cv.removed(CONF_HOLD_LIST),
|
||||
_PLATFORM_SCHEMA_BASE,
|
||||
valid_preset_mode_configuration,
|
||||
valid_humidity_range_configuration,
|
||||
valid_humidity_state_configuration,
|
||||
)
|
||||
|
||||
# Configuring MQTT Climate under the climate platform key was deprecated in HA Core 2022.6
|
||||
@ -337,6 +390,8 @@ DISCOVERY_SCHEMA = vol.All(
|
||||
cv.removed(CONF_HOLD_STATE_TOPIC),
|
||||
cv.removed(CONF_HOLD_LIST),
|
||||
valid_preset_mode_configuration,
|
||||
valid_humidity_range_configuration,
|
||||
valid_humidity_state_configuration,
|
||||
)
|
||||
|
||||
|
||||
@ -396,6 +451,8 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
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]
|
||||
@ -485,6 +542,9 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
):
|
||||
support |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
|
||||
if self._topic[CONF_HUMIDITY_COMMAND_TOPIC] is not None:
|
||||
support |= ClimateEntityFeature.TARGET_HUMIDITY
|
||||
|
||||
if (self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None) or (
|
||||
self._topic[CONF_FAN_MODE_COMMAND_TOPIC] is not None
|
||||
):
|
||||
@ -554,23 +614,23 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received)
|
||||
|
||||
@callback
|
||||
def handle_temperature_received(
|
||||
def handle_climate_attribute_received(
|
||||
msg: ReceiveMessage, template_name: str, attr: str
|
||||
) -> None:
|
||||
"""Handle temperature coming via MQTT."""
|
||||
"""Handle climate attributes coming via MQTT."""
|
||||
payload = render_template(msg, template_name)
|
||||
|
||||
try:
|
||||
setattr(self, attr, float(payload))
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
except ValueError:
|
||||
_LOGGER.error("Could not parse temperature from %s", payload)
|
||||
_LOGGER.error("Could not parse %s from %s", template_name, payload)
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def handle_current_temperature_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle current temperature coming via MQTT."""
|
||||
handle_temperature_received(
|
||||
handle_climate_attribute_received(
|
||||
msg, CONF_CURRENT_TEMP_TEMPLATE, "_attr_current_temperature"
|
||||
)
|
||||
|
||||
@ -582,7 +642,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def handle_target_temperature_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle target temperature coming via MQTT."""
|
||||
handle_temperature_received(
|
||||
handle_climate_attribute_received(
|
||||
msg, CONF_TEMP_STATE_TEMPLATE, "_attr_target_temperature"
|
||||
)
|
||||
|
||||
@ -594,7 +654,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def handle_temperature_low_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle target temperature low coming via MQTT."""
|
||||
handle_temperature_received(
|
||||
handle_climate_attribute_received(
|
||||
msg, CONF_TEMP_LOW_STATE_TEMPLATE, "_attr_target_temperature_low"
|
||||
)
|
||||
|
||||
@ -606,7 +666,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def handle_temperature_high_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle target temperature high coming via MQTT."""
|
||||
handle_temperature_received(
|
||||
handle_climate_attribute_received(
|
||||
msg, CONF_TEMP_HIGH_STATE_TEMPLATE, "_attr_target_temperature_high"
|
||||
)
|
||||
|
||||
@ -614,6 +674,31 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
topics, CONF_TEMP_HIGH_STATE_TOPIC, handle_temperature_high_received
|
||||
)
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def handle_current_humidity_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle current humidity coming via MQTT."""
|
||||
handle_climate_attribute_received(
|
||||
msg, CONF_CURRENT_HUMIDITY_TEMPLATE, "_attr_current_humidity"
|
||||
)
|
||||
|
||||
add_subscription(
|
||||
topics, CONF_CURRENT_HUMIDITY_TOPIC, handle_current_humidity_received
|
||||
)
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def handle_target_humidity_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle target humidity coming via MQTT."""
|
||||
|
||||
handle_climate_attribute_received(
|
||||
msg, CONF_HUMIDITY_STATE_TEMPLATE, "_attr_target_humidity"
|
||||
)
|
||||
|
||||
add_subscription(
|
||||
topics, CONF_HUMIDITY_STATE_TOPIC, handle_target_humidity_received
|
||||
)
|
||||
|
||||
@callback
|
||||
def handle_mode_received(
|
||||
msg: ReceiveMessage, template_name: str, attr: str, mode_list: str
|
||||
@ -744,7 +829,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
self._config[CONF_ENCODING],
|
||||
)
|
||||
|
||||
async def _set_temperature(
|
||||
async def _set_climate_attribute(
|
||||
self,
|
||||
temp: float | None,
|
||||
cmnd_topic: str,
|
||||
@ -770,7 +855,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None:
|
||||
await self.async_set_hvac_mode(operation_mode)
|
||||
|
||||
changed = await self._set_temperature(
|
||||
changed = await self._set_climate_attribute(
|
||||
kwargs.get(ATTR_TEMPERATURE),
|
||||
CONF_TEMP_COMMAND_TOPIC,
|
||||
CONF_TEMP_COMMAND_TEMPLATE,
|
||||
@ -778,7 +863,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
"_attr_target_temperature",
|
||||
)
|
||||
|
||||
changed |= await self._set_temperature(
|
||||
changed |= await self._set_climate_attribute(
|
||||
kwargs.get(ATTR_TARGET_TEMP_LOW),
|
||||
CONF_TEMP_LOW_COMMAND_TOPIC,
|
||||
CONF_TEMP_LOW_COMMAND_TEMPLATE,
|
||||
@ -786,7 +871,7 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
"_attr_target_temperature_low",
|
||||
)
|
||||
|
||||
changed |= await self._set_temperature(
|
||||
changed |= await self._set_climate_attribute(
|
||||
kwargs.get(ATTR_TARGET_TEMP_HIGH),
|
||||
CONF_TEMP_HIGH_COMMAND_TOPIC,
|
||||
CONF_TEMP_HIGH_COMMAND_TEMPLATE,
|
||||
@ -798,6 +883,19 @@ class MqttClimate(MqttEntity, ClimateEntity):
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
|
||||
await self._set_climate_attribute(
|
||||
humidity,
|
||||
CONF_HUMIDITY_COMMAND_TOPIC,
|
||||
CONF_HUMIDITY_COMMAND_TEMPLATE,
|
||||
CONF_HUMIDITY_STATE_TOPIC,
|
||||
"_attr_target_humidity",
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Set new swing mode."""
|
||||
payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](swing_mode)
|
||||
|
@ -9,13 +9,17 @@ import voluptuous as vol
|
||||
from homeassistant.components import climate, mqtt
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_AUX_HEAT,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_HUMIDITY,
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
DEFAULT_MAX_HUMIDITY,
|
||||
DEFAULT_MAX_TEMP,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
DEFAULT_MIN_TEMP,
|
||||
PRESET_ECO,
|
||||
ClimateEntityFeature,
|
||||
@ -67,6 +71,7 @@ DEFAULT_CONFIG = {
|
||||
climate.DOMAIN: {
|
||||
"name": "test",
|
||||
"mode_command_topic": "mode-topic",
|
||||
"target_humidity_command_topic": "humidity-topic",
|
||||
"temperature_command_topic": "temperature-topic",
|
||||
"temperature_low_command_topic": "temperature-low-topic",
|
||||
"temperature_high_command_topic": "temperature-high-topic",
|
||||
@ -108,6 +113,8 @@ async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config):
|
||||
assert state.state == "off"
|
||||
assert state.attributes.get("min_temp") == DEFAULT_MIN_TEMP
|
||||
assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP
|
||||
assert state.attributes.get("min_humidity") == DEFAULT_MIN_HUMIDITY
|
||||
assert state.attributes.get("max_humidity") == DEFAULT_MAX_HUMIDITY
|
||||
|
||||
|
||||
async def test_preset_none_in_preset_modes(hass, caplog):
|
||||
@ -156,6 +163,7 @@ async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config):
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.AUX_HEAT
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
| ClimateEntityFeature.TARGET_HUMIDITY
|
||||
)
|
||||
|
||||
assert state.attributes.get("supported_features") == support
|
||||
@ -489,6 +497,21 @@ async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config):
|
||||
mqtt_mock.async_publish.reset_mock()
|
||||
|
||||
|
||||
async def test_set_target_humidity(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test setting the target humidity."""
|
||||
assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_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("humidity") is None
|
||||
await common.async_set_humidity(hass, humidity=82, entity_id=ENTITY_CLIMATE)
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("humidity") == 82
|
||||
mqtt_mock.async_publish.assert_called_once_with("humidity-topic", "82", 0, False)
|
||||
mqtt_mock.async_publish.reset_mock()
|
||||
|
||||
|
||||
async def test_set_target_temperature_pessimistic(
|
||||
hass, mqtt_mock_entry_with_yaml_config
|
||||
):
|
||||
@ -639,6 +662,53 @@ async def test_set_target_temperature_low_high_optimistic(
|
||||
assert state.attributes.get("target_temp_high") == 25
|
||||
|
||||
|
||||
async def test_set_target_humidity_optimistic(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test setting the target humidity optimistic."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
|
||||
config["climate"]["target_humidity_state_topic"] = "humidity-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("humidity") is None
|
||||
await common.async_set_humidity(hass, humidity=52, entity_id=ENTITY_CLIMATE)
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("humidity") == 52
|
||||
|
||||
async_fire_mqtt_message(hass, "humidity-state", "53")
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("humidity") == 53
|
||||
|
||||
async_fire_mqtt_message(hass, "humidity-state", "not a number")
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("humidity") == 53
|
||||
|
||||
|
||||
async def test_set_target_humidity_pessimistic(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test setting the target humidity."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
|
||||
config["climate"]["target_humidity_state_topic"] = "humidity-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("humidity") is None
|
||||
await common.async_set_humidity(hass, humidity=50, entity_id=ENTITY_CLIMATE)
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("humidity") is None
|
||||
|
||||
async_fire_mqtt_message(hass, "humidity-state", "80")
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("humidity") == 80
|
||||
|
||||
async_fire_mqtt_message(hass, "humidity-state", "not a number")
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("humidity") == 80
|
||||
|
||||
|
||||
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])
|
||||
@ -652,6 +722,36 @@ async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config):
|
||||
assert state.attributes.get("current_temperature") == 47
|
||||
|
||||
|
||||
async def test_receive_mqtt_humidity(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test getting the current humidity via MQTT."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
|
||||
config["climate"]["current_humidity_topic"] = "current_humidity"
|
||||
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
|
||||
await hass.async_block_till_done()
|
||||
await mqtt_mock_entry_with_yaml_config()
|
||||
|
||||
async_fire_mqtt_message(hass, "current_humidity", "35")
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("current_humidity") == 35
|
||||
|
||||
|
||||
async def test_handle_target_humidity_received(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test setting the target humidity via MQTT."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
|
||||
config["climate"]["target_humidity_state_topic"] = "humidity-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("humidity") is None
|
||||
|
||||
async_fire_mqtt_message(hass, "humidity-state", "65")
|
||||
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("humidity") == 65
|
||||
|
||||
|
||||
async def test_handle_action_received(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test getting the action received via MQTT."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
|
||||
@ -929,7 +1029,8 @@ async def test_get_target_temperature_low_high_with_templates(
|
||||
async_fire_mqtt_message(hass, "temperature-state", '"-INVALID-"')
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
# make sure, the invalid value gets logged...
|
||||
assert "Could not parse temperature from" in caplog.text
|
||||
assert "Could not parse temperature_low_state_template from" in caplog.text
|
||||
assert "Could not parse temperature_high_state_template from" in caplog.text
|
||||
# ... but the actual value stays unchanged.
|
||||
assert state.attributes.get("target_temp_low") == 1031
|
||||
assert state.attributes.get("target_temp_high") == 1032
|
||||
@ -951,8 +1052,10 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog
|
||||
config["climate"]["fan_mode_state_topic"] = "fan-state"
|
||||
config["climate"]["swing_mode_state_topic"] = "swing-state"
|
||||
config["climate"]["temperature_state_topic"] = "temperature-state"
|
||||
config["climate"]["target_humidity_state_topic"] = "humidity-state"
|
||||
config["climate"]["aux_state_topic"] = "aux-state"
|
||||
config["climate"]["current_temperature_topic"] = "current-temperature"
|
||||
config["climate"]["current_humidity_topic"] = "current-humidity"
|
||||
config["climate"]["preset_mode_state_topic"] = "current-preset-mode"
|
||||
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
|
||||
await hass.async_block_till_done()
|
||||
@ -986,10 +1089,26 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog
|
||||
async_fire_mqtt_message(hass, "temperature-state", '"-INVALID-"')
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
# make sure, the invalid value gets logged...
|
||||
assert "Could not parse temperature from -INVALID-" in caplog.text
|
||||
assert "Could not parse temperature_state_template from -INVALID-" in caplog.text
|
||||
# ... but the actual value stays unchanged.
|
||||
assert state.attributes.get("temperature") == 1031
|
||||
|
||||
# Humidity - with valid value
|
||||
assert state.attributes.get("humidity") is None
|
||||
async_fire_mqtt_message(hass, "humidity-state", '"82"')
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("humidity") == 82
|
||||
|
||||
# Humidity - with invalid value
|
||||
async_fire_mqtt_message(hass, "humidity-state", '"-INVALID-"')
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
# make sure, the invalid value gets logged...
|
||||
assert (
|
||||
"Could not parse target_humidity_state_template from -INVALID-" in caplog.text
|
||||
)
|
||||
# ... but the actual value stays unchanged.
|
||||
assert state.attributes.get("humidity") == 82
|
||||
|
||||
# Preset Mode
|
||||
assert state.attributes.get("preset_mode") == "none"
|
||||
async_fire_mqtt_message(hass, "current-preset-mode", '{"attribute": "eco"}')
|
||||
@ -1018,6 +1137,11 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("current_temperature") == 74656
|
||||
|
||||
# Current humidity
|
||||
async_fire_mqtt_message(hass, "current-humidity", '"35"')
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("current_humidity") == 35
|
||||
|
||||
# Action
|
||||
async_fire_mqtt_message(hass, "action", '"cooling"')
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
@ -1040,6 +1164,7 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog)
|
||||
config["climate"]["temperature_command_template"] = "temp: {{ value }}"
|
||||
config["climate"]["temperature_high_command_template"] = "temp_hi: {{ value }}"
|
||||
config["climate"]["temperature_low_command_template"] = "temp_lo: {{ value }}"
|
||||
config["climate"]["target_humidity_command_template"] = "humidity: {{ value }}"
|
||||
|
||||
assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
|
||||
await hass.async_block_till_done()
|
||||
@ -1106,6 +1231,15 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog)
|
||||
assert state.attributes.get("target_temp_low") == 20
|
||||
assert state.attributes.get("target_temp_high") == 23
|
||||
|
||||
# Humidity
|
||||
await common.async_set_humidity(hass, humidity=82, entity_id=ENTITY_CLIMATE)
|
||||
mqtt_mock.async_publish.assert_called_once_with(
|
||||
"humidity-topic", "humidity: 82", 0, False
|
||||
)
|
||||
mqtt_mock.async_publish.reset_mock()
|
||||
state = hass.states.get(ENTITY_CLIMATE)
|
||||
assert state.attributes.get("humidity") == 82
|
||||
|
||||
|
||||
async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test a custom min temp."""
|
||||
@ -1139,6 +1273,38 @@ async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config):
|
||||
assert max_temp == 60
|
||||
|
||||
|
||||
async def test_min_humidity_custom(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test a custom min humidity."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
|
||||
config["climate"]["min_humidity"] = 42
|
||||
|
||||
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)
|
||||
min_humidity = state.attributes.get("min_humidity")
|
||||
|
||||
assert isinstance(min_humidity, float)
|
||||
assert state.attributes.get("min_humidity") == 42
|
||||
|
||||
|
||||
async def test_max_humidity_custom(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test a custom max humidity."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
|
||||
config["climate"]["max_humidity"] = 58
|
||||
|
||||
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)
|
||||
max_humidity = state.attributes.get("max_humidity")
|
||||
|
||||
assert isinstance(max_humidity, float)
|
||||
assert max_humidity == 58
|
||||
|
||||
|
||||
async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test a custom temp step."""
|
||||
config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN])
|
||||
@ -1269,6 +1435,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config):
|
||||
("action_topic", "cooling", ATTR_HVAC_ACTION, "cooling"),
|
||||
("aux_state_topic", "ON", ATTR_AUX_HEAT, "on"),
|
||||
("current_temperature_topic", "22.1", ATTR_CURRENT_TEMPERATURE, 22.1),
|
||||
("current_humidity_topic", "60.4", ATTR_CURRENT_HUMIDITY, 60.4),
|
||||
("fan_mode_state_topic", "low", ATTR_FAN_MODE, "low"),
|
||||
("mode_state_topic", "cool", None, None),
|
||||
("mode_state_topic", "fan_only", None, None),
|
||||
@ -1276,6 +1443,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config):
|
||||
("temperature_low_state_topic", "19.1", ATTR_TARGET_TEMP_LOW, 19.1),
|
||||
("temperature_high_state_topic", "22.9", ATTR_TARGET_TEMP_HIGH, 22.9),
|
||||
("temperature_state_topic", "19.9", ATTR_TEMPERATURE, 19.9),
|
||||
("target_humidity_state_topic", "82.6", ATTR_HUMIDITY, 82.6),
|
||||
],
|
||||
)
|
||||
async def test_encoding_subscribable_topics(
|
||||
@ -1545,6 +1713,13 @@ async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config):
|
||||
29.8,
|
||||
"temperature_high_command_template",
|
||||
),
|
||||
(
|
||||
climate.SERVICE_SET_HUMIDITY,
|
||||
"target_humidity_command_topic",
|
||||
{"humidity": "82"},
|
||||
82,
|
||||
"target_humidity_command_template",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_publishing_with_custom_encoding(
|
||||
@ -1578,6 +1753,62 @@ async def test_publishing_with_custom_encoding(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config,valid",
|
||||
[
|
||||
(
|
||||
{
|
||||
"name": "test_valid_humidity_min_max",
|
||||
"min_humidity": 20,
|
||||
"max_humidity": 80,
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "test_invalid_humidity_min_max_1",
|
||||
"min_humidity": 0,
|
||||
"max_humidity": 101,
|
||||
},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "test_invalid_humidity_min_max_2",
|
||||
"max_humidity": 20,
|
||||
"min_humidity": 40,
|
||||
},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "test_valid_humidity_state",
|
||||
"target_humidity_state_topic": "humidity-state",
|
||||
"target_humidity_command_topic": "humidity-command",
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "test_invalid_humidity_state",
|
||||
"target_humidity_state_topic": "humidity-state",
|
||||
},
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_humidity_configuration_validity(hass, config, valid):
|
||||
"""Test the validity of humidity configurations."""
|
||||
assert (
|
||||
await async_setup_component(
|
||||
hass,
|
||||
mqtt.DOMAIN,
|
||||
{mqtt.DOMAIN: {climate.DOMAIN: config}},
|
||||
)
|
||||
is valid
|
||||
)
|
||||
|
||||
|
||||
async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path):
|
||||
"""Test reloading the MQTT platform."""
|
||||
domain = climate.DOMAIN
|
||||
|
Loading…
x
Reference in New Issue
Block a user