Avoid redundant calls to async_write_ha_state
in MQTT climate & water_heater (#100696)
Limit state writes for mqtt climate & water_heater
This commit is contained in:
parent
1041610a70
commit
384adb1c87
4 changed files with 125 additions and 14 deletions
|
@ -82,7 +82,12 @@ from .const import (
|
||||||
PAYLOAD_NONE,
|
PAYLOAD_NONE,
|
||||||
)
|
)
|
||||||
from .debug_info import log_messages
|
from .debug_info import log_messages
|
||||||
from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
|
from .mixins import (
|
||||||
|
MQTT_ENTITY_COMMON_SCHEMA,
|
||||||
|
MqttEntity,
|
||||||
|
async_setup_entry_helper,
|
||||||
|
write_state_on_attr_change,
|
||||||
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
MqttCommandTemplate,
|
MqttCommandTemplate,
|
||||||
MqttValueTemplate,
|
MqttValueTemplate,
|
||||||
|
@ -90,7 +95,7 @@ from .models import (
|
||||||
ReceiveMessage,
|
ReceiveMessage,
|
||||||
ReceivePayloadType,
|
ReceivePayloadType,
|
||||||
)
|
)
|
||||||
from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic
|
from .util import valid_publish_topic, valid_subscribe_topic
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -478,11 +483,9 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||||
return
|
return
|
||||||
if payload == PAYLOAD_NONE:
|
if payload == PAYLOAD_NONE:
|
||||||
setattr(self, attr, None)
|
setattr(self, attr, None)
|
||||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
setattr(self, attr, float(payload))
|
setattr(self, attr, float(payload))
|
||||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
_LOGGER.error("Could not parse %s from %s", template_name, payload)
|
_LOGGER.error("Could not parse %s from %s", template_name, payload)
|
||||||
|
|
||||||
|
@ -493,6 +496,7 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_current_temperature"})
|
||||||
def handle_current_temperature_received(msg: ReceiveMessage) -> None:
|
def handle_current_temperature_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle current temperature coming via MQTT."""
|
"""Handle current temperature coming via MQTT."""
|
||||||
self.handle_climate_attribute_received(
|
self.handle_climate_attribute_received(
|
||||||
|
@ -505,6 +509,7 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_target_temperature"})
|
||||||
def handle_target_temperature_received(msg: ReceiveMessage) -> None:
|
def handle_target_temperature_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle target temperature coming via MQTT."""
|
"""Handle target temperature coming via MQTT."""
|
||||||
self.handle_climate_attribute_received(
|
self.handle_climate_attribute_received(
|
||||||
|
@ -517,6 +522,7 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_target_temperature_low"})
|
||||||
def handle_temperature_low_received(msg: ReceiveMessage) -> None:
|
def handle_temperature_low_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle target temperature low coming via MQTT."""
|
"""Handle target temperature low coming via MQTT."""
|
||||||
self.handle_climate_attribute_received(
|
self.handle_climate_attribute_received(
|
||||||
|
@ -529,6 +535,7 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_target_temperature_high"})
|
||||||
def handle_temperature_high_received(msg: ReceiveMessage) -> None:
|
def handle_temperature_high_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle target temperature high coming via MQTT."""
|
"""Handle target temperature high coming via MQTT."""
|
||||||
self.handle_climate_attribute_received(
|
self.handle_climate_attribute_received(
|
||||||
|
@ -789,6 +796,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_hvac_action"})
|
||||||
def handle_action_received(msg: ReceiveMessage) -> None:
|
def handle_action_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle receiving action via MQTT."""
|
"""Handle receiving action via MQTT."""
|
||||||
payload = self.render_template(msg, CONF_ACTION_TEMPLATE)
|
payload = self.render_template(msg, CONF_ACTION_TEMPLATE)
|
||||||
|
@ -808,12 +816,12 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
payload,
|
payload,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
|
||||||
|
|
||||||
self.add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received)
|
self.add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_current_humidity"})
|
||||||
def handle_current_humidity_received(msg: ReceiveMessage) -> None:
|
def handle_current_humidity_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle current humidity coming via MQTT."""
|
"""Handle current humidity coming via MQTT."""
|
||||||
self.handle_climate_attribute_received(
|
self.handle_climate_attribute_received(
|
||||||
|
@ -825,6 +833,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@write_state_on_attr_change(self, {"_attr_target_humidity"})
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
def handle_target_humidity_received(msg: ReceiveMessage) -> None:
|
def handle_target_humidity_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle target humidity coming via MQTT."""
|
"""Handle target humidity coming via MQTT."""
|
||||||
|
@ -848,10 +857,10 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
_LOGGER.error("Invalid %s mode: %s", mode_list, payload)
|
_LOGGER.error("Invalid %s mode: %s", mode_list, payload)
|
||||||
else:
|
else:
|
||||||
setattr(self, attr, payload)
|
setattr(self, attr, payload)
|
||||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_hvac_mode"})
|
||||||
def handle_current_mode_received(msg: ReceiveMessage) -> None:
|
def handle_current_mode_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle receiving mode via MQTT."""
|
"""Handle receiving mode via MQTT."""
|
||||||
handle_mode_received(
|
handle_mode_received(
|
||||||
|
@ -864,6 +873,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_fan_mode"})
|
||||||
def handle_fan_mode_received(msg: ReceiveMessage) -> None:
|
def handle_fan_mode_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle receiving fan mode via MQTT."""
|
"""Handle receiving fan mode via MQTT."""
|
||||||
handle_mode_received(
|
handle_mode_received(
|
||||||
|
@ -879,6 +889,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_swing_mode"})
|
||||||
def handle_swing_mode_received(msg: ReceiveMessage) -> None:
|
def handle_swing_mode_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle receiving swing mode via MQTT."""
|
"""Handle receiving swing mode via MQTT."""
|
||||||
handle_mode_received(
|
handle_mode_received(
|
||||||
|
@ -913,13 +924,12 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
else:
|
else:
|
||||||
_LOGGER.error("Invalid %s mode: %s", attr, payload)
|
_LOGGER.error("Invalid %s mode: %s", attr, payload)
|
||||||
|
|
||||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
|
||||||
|
|
||||||
# Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC
|
# Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC
|
||||||
# and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9
|
# and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9
|
||||||
# Support will be removed in HA Core 2024.3
|
# Support will be removed in HA Core 2024.3
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_is_aux_heat"})
|
||||||
def handle_aux_mode_received(msg: ReceiveMessage) -> None:
|
def handle_aux_mode_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle receiving aux mode via MQTT."""
|
"""Handle receiving aux mode via MQTT."""
|
||||||
handle_onoff_mode_received(
|
handle_onoff_mode_received(
|
||||||
|
@ -930,12 +940,12 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_preset_mode"})
|
||||||
def handle_preset_mode_received(msg: ReceiveMessage) -> None:
|
def handle_preset_mode_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle receiving preset mode via MQTT."""
|
"""Handle receiving preset mode via MQTT."""
|
||||||
preset_mode = self.render_template(msg, CONF_PRESET_MODE_VALUE_TEMPLATE)
|
preset_mode = self.render_template(msg, CONF_PRESET_MODE_VALUE_TEMPLATE)
|
||||||
if preset_mode in [PRESET_NONE, PAYLOAD_NONE]:
|
if preset_mode in [PRESET_NONE, PAYLOAD_NONE]:
|
||||||
self._attr_preset_mode = PRESET_NONE
|
self._attr_preset_mode = PRESET_NONE
|
||||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
|
||||||
return
|
return
|
||||||
if not preset_mode:
|
if not preset_mode:
|
||||||
_LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic)
|
_LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic)
|
||||||
|
@ -953,8 +963,6 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
else:
|
else:
|
||||||
self._attr_preset_mode = str(preset_mode)
|
self._attr_preset_mode = str(preset_mode)
|
||||||
|
|
||||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
|
||||||
|
|
||||||
self.add_subscription(
|
self.add_subscription(
|
||||||
topics, CONF_PRESET_MODE_STATE_TOPIC, handle_preset_mode_received
|
topics, CONF_PRESET_MODE_STATE_TOPIC, handle_preset_mode_received
|
||||||
)
|
)
|
||||||
|
|
|
@ -65,9 +65,13 @@ from .const import (
|
||||||
DEFAULT_OPTIMISTIC,
|
DEFAULT_OPTIMISTIC,
|
||||||
)
|
)
|
||||||
from .debug_info import log_messages
|
from .debug_info import log_messages
|
||||||
from .mixins import MQTT_ENTITY_COMMON_SCHEMA, async_setup_entry_helper
|
from .mixins import (
|
||||||
|
MQTT_ENTITY_COMMON_SCHEMA,
|
||||||
|
async_setup_entry_helper,
|
||||||
|
write_state_on_attr_change,
|
||||||
|
)
|
||||||
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
|
from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
|
||||||
from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic
|
from .util import valid_publish_topic, valid_subscribe_topic
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -292,10 +296,10 @@ class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity):
|
||||||
_LOGGER.error("Invalid %s mode: %s", mode_list, payload)
|
_LOGGER.error("Invalid %s mode: %s", mode_list, payload)
|
||||||
else:
|
else:
|
||||||
setattr(self, attr, payload)
|
setattr(self, attr, payload)
|
||||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@write_state_on_attr_change(self, {"_attr_current_operation"})
|
||||||
def handle_current_mode_received(msg: ReceiveMessage) -> None:
|
def handle_current_mode_received(msg: ReceiveMessage) -> None:
|
||||||
"""Handle receiving operation mode via MQTT."""
|
"""Handle receiving operation mode via MQTT."""
|
||||||
handle_mode_received(
|
handle_mode_received(
|
||||||
|
|
|
@ -58,6 +58,7 @@ from .test_common import (
|
||||||
help_test_setting_attribute_via_mqtt_json_message,
|
help_test_setting_attribute_via_mqtt_json_message,
|
||||||
help_test_setting_attribute_with_template,
|
help_test_setting_attribute_with_template,
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
|
help_test_skipped_async_ha_write_state,
|
||||||
help_test_unique_id,
|
help_test_unique_id,
|
||||||
help_test_unload_config_entry_with_platform,
|
help_test_unload_config_entry_with_platform,
|
||||||
help_test_update_with_json_attrs_bad_json,
|
help_test_update_with_json_attrs_bad_json,
|
||||||
|
@ -2555,3 +2556,60 @@ async def test_unload_entry(
|
||||||
await help_test_unload_config_entry_with_platform(
|
await help_test_unload_config_entry_with_platform(
|
||||||
hass, mqtt_mock_entry, domain, config
|
hass, mqtt_mock_entry, domain, config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"hass_config",
|
||||||
|
[
|
||||||
|
help_custom_config(
|
||||||
|
climate.DOMAIN,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"availability_topic": "availability-topic",
|
||||||
|
"json_attributes_topic": "json-attributes-topic",
|
||||||
|
"action_topic": "action-topic",
|
||||||
|
"fan_mode_state_topic": "fan-mode-state-topic",
|
||||||
|
"mode_state_topic": "mode-state-topic",
|
||||||
|
"current_humidity_topic": "current-humidity-topic",
|
||||||
|
"current_temperature_topic": "current-temperature-topic",
|
||||||
|
"preset_mode_state_topic": "preset-mode-state-topic",
|
||||||
|
"preset_modes": ["eco", "away"],
|
||||||
|
"swing_mode_state_topic": "swing-mode-state-topic",
|
||||||
|
"target_humidity_state_topic": "target-humidity-state-topic",
|
||||||
|
"temperature_high_state_topic": "temperature-high-state-topic",
|
||||||
|
"temperature_low_state_topic": "temperature-low-state-topic",
|
||||||
|
"temperature_state_topic": "temperature-state-topic",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("topic", "payload1", "payload2"),
|
||||||
|
[
|
||||||
|
("availability-topic", "online", "offline"),
|
||||||
|
("json-attributes-topic", '{"attr1": "val1"}', '{"attr1": "val2"}'),
|
||||||
|
("action-topic", "cooling", "heating"),
|
||||||
|
("fan-mode-state-topic", "low", "medium"),
|
||||||
|
("mode-state-topic", "cool", "heat"),
|
||||||
|
("current-humidity-topic", "45", "46"),
|
||||||
|
("current-temperature-topic", "18.0", "18.1"),
|
||||||
|
("preset-mode-state-topic", "eco", "away"),
|
||||||
|
("swing-mode-state-topic", "on", "off"),
|
||||||
|
("target-humidity-state-topic", "45", "50"),
|
||||||
|
("temperature-state-topic", "18", "19"),
|
||||||
|
("temperature-low-state-topic", "18", "19"),
|
||||||
|
("temperature-high-state-topic", "18", "19"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_skipped_async_ha_write_state(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||||
|
topic: str,
|
||||||
|
payload1: str,
|
||||||
|
payload2: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test a write state command is only called when there is change."""
|
||||||
|
await mqtt_mock_entry()
|
||||||
|
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2)
|
||||||
|
|
|
@ -52,6 +52,7 @@ from .test_common import (
|
||||||
help_test_setting_attribute_via_mqtt_json_message,
|
help_test_setting_attribute_via_mqtt_json_message,
|
||||||
help_test_setting_attribute_with_template,
|
help_test_setting_attribute_with_template,
|
||||||
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
help_test_setting_blocked_attribute_via_mqtt_json_message,
|
||||||
|
help_test_skipped_async_ha_write_state,
|
||||||
help_test_unique_id,
|
help_test_unique_id,
|
||||||
help_test_unload_config_entry_with_platform,
|
help_test_unload_config_entry_with_platform,
|
||||||
help_test_update_with_json_attrs_bad_json,
|
help_test_update_with_json_attrs_bad_json,
|
||||||
|
@ -1220,3 +1221,43 @@ async def test_unload_entry(
|
||||||
await help_test_unload_config_entry_with_platform(
|
await help_test_unload_config_entry_with_platform(
|
||||||
hass, mqtt_mock_entry, domain, config
|
hass, mqtt_mock_entry, domain, config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"hass_config",
|
||||||
|
[
|
||||||
|
help_custom_config(
|
||||||
|
water_heater.DOMAIN,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"availability_topic": "availability-topic",
|
||||||
|
"json_attributes_topic": "json-attributes-topic",
|
||||||
|
"mode_state_topic": "mode-state-topic",
|
||||||
|
"current_temperature_topic": "current-temperature-topic",
|
||||||
|
"temperature_state_topic": "temperature-state-topic",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("topic", "payload1", "payload2"),
|
||||||
|
[
|
||||||
|
("availability-topic", "online", "offline"),
|
||||||
|
("json-attributes-topic", '{"attr1": "val1"}', '{"attr1": "val2"}'),
|
||||||
|
("mode-state-topic", "gas", "electric"),
|
||||||
|
("current-temperature-topic", "18.0", "18.1"),
|
||||||
|
("temperature-state-topic", "18", "19"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_skipped_async_ha_write_state(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||||
|
topic: str,
|
||||||
|
payload1: str,
|
||||||
|
payload2: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test a write state command is only called when there is change."""
|
||||||
|
await mqtt_mock_entry()
|
||||||
|
await help_test_skipped_async_ha_write_state(hass, topic, payload1, payload2)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue