mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Fix state saving when sharing topics for MQTT entities (#79421)
* Do not write old state sharing availability topic * Add a test * Support for all availability topics * delay async_write_ha_state till last callback * Process write req after processing callback jobs * Do not count subscription callbacks * Simplify * Stale docsting * No topic needed for delays state write * No need to clear when reloading * Move test to test_mixins.py * Only set up sensor platform for test
This commit is contained in:
parent
6f7cb158d8
commit
8aa30cce26
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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"] = {
|
||||
|
@ -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,
|
||||
|
@ -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] = {
|
||||
|
@ -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] = {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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"] = {
|
||||
|
73
tests/components/mqtt/test_mixins.py
Normal file
73
tests/components/mqtt/test_mixins.py
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user