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:
Jan Bouwhuis 2023-03-06 15:34:47 +01:00 committed by GitHub
parent 0c042e8f72
commit 5ee383456f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 84 additions and 13 deletions

View file

@ -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,

View file

@ -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:

View file

@ -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,

View file

@ -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")