Catch exceptions and add logging when writing states on MQTT entities (#89091)
* Catch exceptions when writing states * Do not use wrapper for logging and adjust tests * Catch logging directly on async_write_ha_state() * Update homeassistant/components/mqtt/models.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Fix test --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
0c042e8f72
commit
5ee383456f
4 changed files with 84 additions and 13 deletions
|
@ -719,7 +719,7 @@ class MQTT:
|
||||||
timestamp,
|
timestamp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._mqtt_data.state_write_requests.process_write_state_requests()
|
self._mqtt_data.state_write_requests.process_write_state_requests(msg)
|
||||||
|
|
||||||
def _mqtt_on_callback(
|
def _mqtt_on_callback(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -21,6 +21,8 @@ from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from paho.mqtt.client import MQTTMessage
|
||||||
|
|
||||||
from .client import MQTT, Subscription
|
from .client import MQTT, Subscription
|
||||||
from .debug_info import TimestampedPublishMessage
|
from .debug_info import TimestampedPublishMessage
|
||||||
from .device_trigger import Trigger
|
from .device_trigger import Trigger
|
||||||
|
@ -260,11 +262,21 @@ class EntityTopicState:
|
||||||
self.subscribe_calls: dict[str, Entity] = {}
|
self.subscribe_calls: dict[str, Entity] = {}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def process_write_state_requests(self) -> None:
|
def process_write_state_requests(self, msg: MQTTMessage) -> None:
|
||||||
"""Process the write state requests."""
|
"""Process the write state requests."""
|
||||||
while self.subscribe_calls:
|
while self.subscribe_calls:
|
||||||
_, entity = self.subscribe_calls.popitem()
|
_, entity = self.subscribe_calls.popitem()
|
||||||
entity.async_write_ha_state()
|
try:
|
||||||
|
entity.async_write_ha_state()
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.error(
|
||||||
|
"Exception raised when updating state of %s, topic: "
|
||||||
|
"'%s' with payload: %s",
|
||||||
|
entity.entity_id,
|
||||||
|
msg.topic,
|
||||||
|
msg.payload,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def write_state_request(self, entity: Entity) -> None:
|
def write_state_request(self, entity: Entity) -> None:
|
||||||
|
|
|
@ -817,6 +817,50 @@ def test_entity_device_info_schema() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"hass_config",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
mqtt.DOMAIN: {
|
||||||
|
"sensor": [
|
||||||
|
{
|
||||||
|
"name": "test-sensor",
|
||||||
|
"unique_id": "test-sensor",
|
||||||
|
"state_topic": "test/state",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_handle_logging_on_writing_the_entity_state(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_hass_config: None,
|
||||||
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test on log handling when an error occurs writing the state."""
|
||||||
|
await mqtt_mock_entry_no_yaml_config()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
async_fire_mqtt_message(hass, "test/state", b"initial_state")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.test_sensor")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == "initial_state"
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.entity.Entity.async_write_ha_state",
|
||||||
|
side_effect=ValueError("Invalid value for sensor"),
|
||||||
|
):
|
||||||
|
async_fire_mqtt_message(hass, "test/state", b"payload causing errors")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get("sensor.test_sensor")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == "initial_state"
|
||||||
|
assert "Invalid value for sensor" in caplog.text
|
||||||
|
assert "Exception raised when updating state of" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
async def test_receiving_non_utf8_message_gets_logged(
|
async def test_receiving_non_utf8_message_gets_logged(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
mqtt_mock_entry_no_yaml_config: MqttMockHAClientGenerator,
|
||||||
|
|
|
@ -116,7 +116,9 @@ async def test_controlling_state_via_topic(
|
||||||
|
|
||||||
|
|
||||||
async def test_controlling_validation_state_via_topic(
|
async def test_controlling_validation_state_via_topic(
|
||||||
hass: HomeAssistant, mqtt_mock_entry_with_yaml_config: MqttMockHAClientGenerator
|
hass: HomeAssistant,
|
||||||
|
mqtt_mock_entry_with_yaml_config: MqttMockHAClientGenerator,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the validation of a received state."""
|
"""Test the validation of a received state."""
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
|
@ -148,26 +150,39 @@ async def test_controlling_validation_state_via_topic(
|
||||||
assert state.state == "yes"
|
assert state.state == "yes"
|
||||||
|
|
||||||
# test pattern error
|
# test pattern error
|
||||||
with pytest.raises(ValueError):
|
caplog.clear()
|
||||||
async_fire_mqtt_message(hass, "state-topic", "other")
|
async_fire_mqtt_message(hass, "state-topic", "other")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
assert (
|
||||||
|
"ValueError: Entity text.test provides state other which does not match expected pattern (y|n)"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
state = hass.states.get("text.test")
|
state = hass.states.get("text.test")
|
||||||
assert state.state == "yes"
|
assert state.state == "yes"
|
||||||
|
|
||||||
# test text size to large
|
# test text size to large
|
||||||
with pytest.raises(ValueError):
|
caplog.clear()
|
||||||
async_fire_mqtt_message(hass, "state-topic", "yesyesyesyes")
|
async_fire_mqtt_message(hass, "state-topic", "yesyesyesyes")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
assert (
|
||||||
|
"ValueError: Entity text.test provides state yesyesyesyes which is too long (maximum length 10)"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
state = hass.states.get("text.test")
|
state = hass.states.get("text.test")
|
||||||
assert state.state == "yes"
|
assert state.state == "yes"
|
||||||
|
|
||||||
# test text size to small
|
# test text size to small
|
||||||
with pytest.raises(ValueError):
|
caplog.clear()
|
||||||
async_fire_mqtt_message(hass, "state-topic", "y")
|
async_fire_mqtt_message(hass, "state-topic", "y")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
assert (
|
||||||
|
"ValueError: Entity text.test provides state y which is too short (minimum length 2)"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
state = hass.states.get("text.test")
|
state = hass.states.get("text.test")
|
||||||
assert state.state == "yes"
|
assert state.state == "yes"
|
||||||
|
|
||||||
|
# test with valid text
|
||||||
async_fire_mqtt_message(hass, "state-topic", "no")
|
async_fire_mqtt_message(hass, "state-topic", "no")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get("text.test")
|
state = hass.states.get("text.test")
|
||||||
|
|
Loading…
Add table
Reference in a new issue