diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c3502cd8e64..ed1990d919e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -49,7 +49,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -211,7 +211,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): _LOGGER.warning("Received unexpected payload: %s", msg.payload) return self._state = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index f0e5ecc9df8..915a2780283 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -47,6 +47,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -260,7 +261,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): self.hass, off_delay, off_delay_listener ) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index b0ce53d75fd..bf3f24c950e 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -659,6 +659,7 @@ class MQTT: timestamp, ), ) + self._mqtt_data.state_write_requests.process_write_state_requests() def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None: """Publish / Subscribe / Unsubscribe callback.""" diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 96c7ca3665b..9f98fcfebdc 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -55,7 +55,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -494,7 +494,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload, ) return - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) @@ -505,7 +505,7 @@ class MqttClimate(MqttEntity, ClimateEntity): try: setattr(self, attr, float(payload)) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: _LOGGER.error("Could not parse temperature from %s", payload) @@ -564,7 +564,7 @@ class MqttClimate(MqttEntity, ClimateEntity): _LOGGER.error("Invalid %s mode: %s", mode_list, payload) else: setattr(self, attr, payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -623,7 +623,7 @@ class MqttClimate(MqttEntity, ClimateEntity): else: _LOGGER.error("Invalid %s mode: %s", attr, payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -640,7 +640,7 @@ class MqttClimate(MqttEntity, ClimateEntity): preset_mode = render_template(msg, CONF_PRESET_MODE_VALUE_TEMPLATE) if preset_mode in [PRESET_NONE, PAYLOAD_NONE]: self._preset_mode = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not preset_mode: _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) @@ -654,7 +654,7 @@ class MqttClimate(MqttEntity, ClimateEntity): ) else: self._preset_mode = preset_mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_subscription( topics, CONF_PRESET_MODE_STATE_TOPIC, handle_preset_mode_received diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 1f5d26c3a78..6ed12b8adef 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -51,7 +51,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -405,7 +405,7 @@ class MqttCover(MqttEntity, CoverEntity): ) return - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @callback @log_messages(self.hass, self.entity_id) @@ -451,7 +451,7 @@ class MqttCover(MqttEntity, CoverEntity): else STATE_OPEN ) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_GET_POSITION_TOPIC): topics["get_position_topic"] = { diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 907d424e8a4..1d3f9d109f6 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -29,6 +29,7 @@ from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper from ..models import MqttValueTemplate +from ..util import get_mqtt_data CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" @@ -106,7 +107,7 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): else: self._location_name = msg.payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 584df08e7d7..866b429c68f 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -55,7 +55,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" @@ -391,7 +391,7 @@ class MqttFan(MqttEntity, FanEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -413,7 +413,7 @@ class MqttFan(MqttEntity, FanEntity): return if rendered_percentage_payload == self._payload["PERCENTAGE_RESET"]: self._percentage = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: percentage = ranged_value_to_percentage( @@ -436,7 +436,7 @@ class MqttFan(MqttEntity, FanEntity): ) return self._percentage = percentage - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PERCENTAGE_STATE_TOPIC] is not None: topics[CONF_PERCENTAGE_STATE_TOPIC] = { @@ -469,7 +469,7 @@ class MqttFan(MqttEntity, FanEntity): return self._preset_mode = preset_mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None: topics[CONF_PRESET_MODE_STATE_TOPIC] = { @@ -492,7 +492,7 @@ class MqttFan(MqttEntity, FanEntity): self._oscillation = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: self._oscillation = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: topics[CONF_OSCILLATION_STATE_TOPIC] = { diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 837bbb8b909..7514f0ff672 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -51,7 +51,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate -from .util import valid_publish_topic, valid_subscribe_topic +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_AVAILABLE_MODES_LIST = "modes" CONF_DEVICE_CLASS = "device_class" @@ -309,7 +309,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -331,7 +331,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): return if rendered_target_humidity_payload == self._payload["HUMIDITY_RESET"]: self._target_humidity = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: target_humidity = round(float(rendered_target_humidity_payload)) @@ -355,7 +355,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ) return self._target_humidity = target_humidity - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is not None: topics[CONF_TARGET_HUMIDITY_STATE_TOPIC] = { @@ -373,7 +373,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): mode = self._value_templates[ATTR_MODE](msg.payload) if mode == self._payload["MODE_RESET"]: self._mode = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not mode: _LOGGER.debug("Ignoring empty mode from '%s'", msg.topic) @@ -388,7 +388,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): return self._mode = mode - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_MODE_STATE_TOPIC] is not None: topics[CONF_MODE_STATE_TOPIC] = { diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index e2805781f45..d435d4e91ad 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -51,7 +51,7 @@ from ..const import ( from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from ..models import MqttCommandTemplate, MqttValueTemplate -from ..util import valid_publish_topic, valid_subscribe_topic +from ..util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -438,7 +438,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._state = False elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: topics[CONF_STATE_TOPIC] = { @@ -462,7 +462,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): device_value = float(payload) percent_bright = device_value / self._config[CONF_BRIGHTNESS_SCALE] self._brightness = percent_bright * 255 - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) @@ -493,7 +493,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgb: return self._rgb_color = rgb - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGB_STATE_TOPIC, rgb_received) @@ -510,7 +510,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgbw: return self._rgbw_color = rgbw - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBW_STATE_TOPIC, rgbw_received) @@ -527,7 +527,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if not rgbww: return self._rgbww_color = rgbww - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBWW_STATE_TOPIC, rgbww_received) @@ -543,7 +543,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return self._color_mode = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_MODE_STATE_TOPIC, color_mode_received) @@ -561,7 +561,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.COLOR_TEMP self._color_temp = int(payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) @@ -577,7 +577,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return self._effect = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) @@ -594,7 +594,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.HS self._hs_color = hs_color - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: _LOGGER.debug("Failed to parse hs state update: '%s'", payload) @@ -613,7 +613,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if self._optimistic_color_mode: self._color_mode = ColorMode.XY self._xy_color = xy_color - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_XY_STATE_TOPIC, xy_received) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 8843e8542eb..a4a76673176 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -58,7 +58,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity -from ..util import valid_subscribe_topic +from ..util import get_mqtt_data, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import ( CONF_BRIGHTNESS_SCALE, @@ -401,7 +401,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): with suppress(KeyError): self._effect = values["effect"] - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index dacc977a036..33c7f1cea1b 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -41,6 +41,7 @@ from ..const import ( from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from ..models import MqttValueTemplate +from ..util import get_mqtt_data from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -256,7 +257,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): else: _LOGGER.warning("Unsupported effect value received") - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topics[CONF_STATE_TOPIC] is not None: self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index dca02f909dc..c9bdd696896 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -33,6 +33,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" @@ -158,7 +159,7 @@ class MqttLock(MqttEntity, LockEntity): elif payload == self._config[CONF_STATE_UNLOCKED]: self._state = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 8022a6e91ae..b5c870a196e 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -435,7 +435,9 @@ class MqttAttributes(Entity): and k not in self._attributes_extra_blocked } self._attributes = filtered_dict - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request( + self + ) else: _LOGGER.warning("JSON result was not a dictionary") self._attributes = None @@ -547,7 +549,7 @@ class MqttAvailability(Entity): self._available[topic] = False self._available_latest = False - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._available = { topic: (self._available[topic] if topic in self._available else False) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index b7cb81b2ea4..f2f30419b4c 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -236,6 +236,26 @@ class MqttValueTemplate: ) +class EntityTopicState: + """Manage entity state write requests for subscribed topics.""" + + def __init__(self) -> None: + """Register topic.""" + self.subscribe_calls: dict[str, Entity] = {} + + @callback + def process_write_state_requests(self) -> None: + """Process the write state requests.""" + while self.subscribe_calls: + _, entity = self.subscribe_calls.popitem() + entity.async_write_ha_state() + + @callback + def write_state_request(self, entity: Entity) -> None: + """Register write state request.""" + self.subscribe_calls[entity.entity_id] = entity + + @dataclass class MqttData: """Keep the MQTT entry data.""" @@ -264,6 +284,7 @@ class MqttData: default_factory=dict ) reload_needed: bool = False + state_write_requests: EntityTopicState = field(default_factory=EntityTopicState) subscriptions_to_restore: list[Subscription] = field(default_factory=list) tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) updated_config: ConfigType = field(default_factory=dict) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 09f9d122b98..25ef7af8d6e 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -49,6 +49,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -222,7 +223,7 @@ class MqttNumber(MqttEntity, RestoreNumber): return self._current_number = num_value - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index a6de0495690..12593550e2f 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -35,6 +35,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -169,7 +170,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): return self._attr_current_option = payload - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index b3869cb8afe..d95d669e72f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -46,7 +46,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate -from .util import valid_subscribe_topic +from .util import get_mqtt_data, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -300,7 +300,7 @@ class MqttSensor(MqttEntity, RestoreSensor): or self._config[CONF_LAST_RESET_TOPIC] == self._config[CONF_STATE_TOPIC] ): _update_last_reset(msg) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) topics["state_topic"] = { "topic": self._config[CONF_STATE_TOPIC], @@ -314,7 +314,7 @@ class MqttSensor(MqttEntity, RestoreSensor): def last_reset_message_received(msg): """Handle new last_reset messages.""" _update_last_reset(msg) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if ( CONF_LAST_RESET_TOPIC in self._config diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index c8332046092..2ab226e44c0 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -54,6 +54,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttCommandTemplate, MqttValueTemplate +from .util import get_mqtt_data DEFAULT_NAME = "MQTT Siren" DEFAULT_PAYLOAD_ON = "ON" @@ -283,7 +284,7 @@ class MqttSiren(MqttEntity, SirenEntity): ) return self._update(process_turn_on_params(self, json_payload)) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index d0af533f294..05f7f3934ee 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -26,9 +26,12 @@ class EntitySubscription: qos: int = attr.ib(default=0) encoding: str = attr.ib(default="utf-8") - def resubscribe_if_necessary(self, hass, other): + def resubscribe_if_necessary( + self, hass: HomeAssistant, other: EntitySubscription | None + ) -> None: """Re-subscribe to the new topic if necessary.""" if not self._should_resubscribe(other): + assert other self.unsubscribe_callback = other.unsubscribe_callback return @@ -56,7 +59,7 @@ class EntitySubscription: return self.unsubscribe_callback = await self.subscribe_task - def _should_resubscribe(self, other): + def _should_resubscribe(self, other: EntitySubscription | None) -> bool: """Check if we should re-subscribe to the topic using the old state.""" if other is None: return True diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index af16b14bea1..f8bf2f5bc6a 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -47,6 +47,7 @@ from .mixins import ( warn_for_legacy_schema, ) from .models import MqttValueTemplate +from .util import get_mqtt_data DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" @@ -168,7 +169,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): elif payload == PAYLOAD_NONE: self._state = None - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 6b957aded5c..09c4448fda7 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -20,7 +20,7 @@ from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema from ..models import MqttValueTemplate -from ..util import valid_publish_topic +from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -320,7 +320,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if fan_speed: self._fan_speed = fan_speed - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) topics_list = {topic for topic in self._state_topics.values() if topic} self._sub_state = subscription.async_prepare_subscribe_topics( diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index af6c8d289d8..8dfaba80109 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -32,7 +32,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema -from ..util import valid_publish_topic +from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -211,7 +211,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): ) del payload[STATE] self._state_attrs.update(payload) - self.async_write_ha_state() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC): topics["state_position_topic"] = { diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py new file mode 100644 index 00000000000..97a959b8dbe --- /dev/null +++ b/tests/components/mqtt/test_mixins.py @@ -0,0 +1,73 @@ +"""The tests for shared code of the MQTT platform.""" + +from unittest.mock import patch + +from homeassistant.components import mqtt, sensor +from homeassistant.const import EVENT_STATE_CHANGED, Platform +import homeassistant.core as ha +from homeassistant.setup import async_setup_component + +from tests.common import async_fire_mqtt_message + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_availability_with_shared_state_topic( + hass, + mqtt_mock_entry_with_yaml_config, +): + """Test the state is not changed twice. + + When an entity with a shared state_topic and availability_topic becomes available + The state should only change once. + """ + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "availability_topic": "test-topic", + "payload_available": True, + "payload_not_available": False, + "value_template": "{{ int(value) or '' }}", + "availability_template": "{{ value != '0' }}", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + events = [] + + @ha.callback + def callback(event): + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message(hass, "test-topic", "100") + await hass.async_block_till_done() + # Initially the state and the availability change + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "50") + await hass.async_block_till_done() + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "0") + await hass.async_block_till_done() + # Only the availability is changed since the template resukts in an empty payload + # This does not change the state + assert len(events) == 1 + + events.clear() + async_fire_mqtt_message(hass, "test-topic", "10") + await hass.async_block_till_done() + # The availability is changed but the topic is shared, + # hence there the state will be written when the value is updated + assert len(events) == 1