Update MQTT cover template handling (#50236)

* flake 8

* Implement feedback from PR

* update warning message

* added and updated tests

* remove _has_tilt_topic variable

* flake 8

* Implement feedback from PR

* update warning message

* added and updated tests

* remove _has_tilt_topic variable

* renamed _tilt_message_received to _tilt_payload_received

* merged with latesed upstream/dev

* converted if to try except for type check

* Implemented the suggestions of @emontnemery

* Tweak tests

* logger info to debug

Co-authored-by: Shay Levy <levyshay1@gmail.com>

* cast tilt payload as int; combine exceptions to one line

* Add test for JSONDecodeError

* Update homeassistant/components/mqtt/cover.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
This commit is contained in:
Michael Klamminger 2021-05-17 19:31:11 +02:00 committed by GitHub
parent 72dfa8606e
commit 3554316f3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 532 additions and 50 deletions

View File

@ -1,5 +1,6 @@
"""Support for MQTT cover devices.""" """Support for MQTT cover devices."""
import functools import functools
from json import JSONDecodeError, loads as json_loads
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -252,7 +253,7 @@ class MqttCover(MqttEntity, CoverEntity):
if tilt_status_template is not None: if tilt_status_template is not None:
tilt_status_template.hass = self.hass tilt_status_template.hass = self.hass
async def _subscribe_topics(self): async def _subscribe_topics(self): # noqa: C901
"""(Re)Subscribe to topics.""" """(Re)Subscribe to topics."""
topics = {} topics = {}
@ -261,45 +262,36 @@ class MqttCover(MqttEntity, CoverEntity):
def tilt_message_received(msg): def tilt_message_received(msg):
"""Handle tilt updates.""" """Handle tilt updates."""
payload = msg.payload payload = msg.payload
tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) template = self._config.get(CONF_TILT_STATUS_TEMPLATE)
if tilt_status_template is not None: if template is not None:
payload = tilt_status_template.async_render_with_possible_json_value( variables = {
payload "entity_id": self.entity_id,
"position_open": self._config[CONF_POSITION_OPEN],
"position_closed": self._config[CONF_POSITION_CLOSED],
"tilt_min": self._config[CONF_TILT_MIN],
"tilt_max": self._config[CONF_TILT_MAX],
}
payload = template.async_render_with_possible_json_value(
payload, variables=variables
) )
if not payload: if not payload:
_LOGGER.debug("Ignoring empty tilt message from '%s'", msg.topic) _LOGGER.debug("Ignoring empty tilt message from '%s'", msg.topic)
return return
if not payload.isnumeric(): self.tilt_payload_received(payload)
_LOGGER.warning("Payload '%s' is not numeric", payload)
elif (
self._config[CONF_TILT_MIN]
<= int(payload)
<= self._config[CONF_TILT_MAX]
or self._config[CONF_TILT_MAX]
<= int(payload)
<= self._config[CONF_TILT_MIN]
):
level = self.find_percentage_in_range(float(payload))
self._tilt_value = level
self.async_write_ha_state()
else:
_LOGGER.warning(
"Payload '%s' is out of range, must be between '%s' and '%s' inclusive",
payload,
self._config[CONF_TILT_MIN],
self._config[CONF_TILT_MAX],
)
@callback @callback
@log_messages(self.hass, self.entity_id) @log_messages(self.hass, self.entity_id)
def state_message_received(msg): def state_message_received(msg):
"""Handle new MQTT state messages.""" """Handle new MQTT state messages."""
payload = msg.payload payload = msg.payload
value_template = self._config.get(CONF_VALUE_TEMPLATE) template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None: if template is not None:
payload = value_template.async_render_with_possible_json_value(payload) variables = {"entity_id": self.entity_id}
payload = template.async_render_with_possible_json_value(
payload, variables=variables
)
if not payload: if not payload:
_LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic)
@ -347,26 +339,57 @@ class MqttCover(MqttEntity, CoverEntity):
template = self._config.get(CONF_VALUE_TEMPLATE) template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None: if template is not None:
payload = template.async_render_with_possible_json_value(payload) variables = {
"entity_id": self.entity_id,
"position_open": self._config[CONF_POSITION_OPEN],
"position_closed": self._config[CONF_POSITION_CLOSED],
"tilt_min": self._config[CONF_TILT_MIN],
"tilt_max": self._config[CONF_TILT_MAX],
}
payload = template.async_render_with_possible_json_value(
payload, variables=variables
)
if not payload: if not payload:
_LOGGER.debug("Ignoring empty position message from '%s'", msg.topic) _LOGGER.debug(
return "Ignoring empty position message from '%s'", msg.topic
)
return
if payload.isnumeric(): try:
payload = json_loads(payload)
except JSONDecodeError:
pass
if isinstance(payload, dict):
if "position" not in payload:
_LOGGER.warning(
"Template (position_template) returned JSON without position attribute"
)
return
if "tilt_position" in payload:
if not self._config.get(CONF_TILT_STATE_OPTIMISTIC):
# reset forced set tilt optimistic
self._tilt_optimistic = False
self.tilt_payload_received(payload["tilt_position"])
payload = payload["position"]
try:
percentage_payload = self.find_percentage_in_range( percentage_payload = self.find_percentage_in_range(
float(payload), COVER_PAYLOAD float(payload), COVER_PAYLOAD
) )
self._position = percentage_payload except ValueError:
if self._config.get(CONF_STATE_TOPIC) is None:
self._state = (
STATE_CLOSED
if percentage_payload == DEFAULT_POSITION_CLOSED
else STATE_OPEN
)
else:
_LOGGER.warning("Payload '%s' is not numeric", payload) _LOGGER.warning("Payload '%s' is not numeric", payload)
return return
self._position = percentage_payload
if self._config.get(CONF_STATE_TOPIC) is None:
self._state = (
STATE_CLOSED
if percentage_payload == DEFAULT_POSITION_CLOSED
else STATE_OPEN
)
self.async_write_ha_state() self.async_write_ha_state()
if self._config.get(CONF_GET_POSITION_TOPIC): if self._config.get(CONF_GET_POSITION_TOPIC):
@ -391,6 +414,7 @@ class MqttCover(MqttEntity, CoverEntity):
self._optimistic = True self._optimistic = True
if self._config.get(CONF_TILT_STATUS_TOPIC) is None: if self._config.get(CONF_TILT_STATUS_TOPIC) is None:
# Force into optimistic tilt mode.
self._tilt_optimistic = True self._tilt_optimistic = True
else: else:
self._tilt_value = STATE_UNKNOWN self._tilt_value = STATE_UNKNOWN
@ -550,12 +574,21 @@ class MqttCover(MqttEntity, CoverEntity):
async def async_set_cover_tilt_position(self, **kwargs): async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position.""" """Move the cover tilt to a specific position."""
set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE) template = self._config.get(CONF_TILT_COMMAND_TEMPLATE)
tilt = kwargs[ATTR_TILT_POSITION] tilt = kwargs[ATTR_TILT_POSITION]
percentage_tilt = tilt percentage_tilt = tilt
tilt = self.find_in_range_from_percent(tilt) tilt = self.find_in_range_from_percent(tilt)
if set_tilt_template is not None: # Handover the tilt after calculated from percent would make it more consistent with receiving templates
tilt = set_tilt_template.async_render(parse_result=False, **kwargs) if template is not None:
variables = {
"tilt_position": percentage_tilt,
"entity_id": self.entity_id,
"position_open": self._config[CONF_POSITION_OPEN],
"position_closed": self._config[CONF_POSITION_CLOSED],
"tilt_min": self._config[CONF_TILT_MIN],
"tilt_max": self._config[CONF_TILT_MAX],
}
tilt = template.async_render(parse_result=False, variables=variables)
mqtt.async_publish( mqtt.async_publish(
self.hass, self.hass,
@ -565,17 +598,26 @@ class MqttCover(MqttEntity, CoverEntity):
self._config[CONF_RETAIN], self._config[CONF_RETAIN],
) )
if self._tilt_optimistic: if self._tilt_optimistic:
_LOGGER.debug("Set tilt value optimistic")
self._tilt_value = percentage_tilt self._tilt_value = percentage_tilt
self.async_write_ha_state() self.async_write_ha_state()
async def async_set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE) template = self._config.get(CONF_SET_POSITION_TEMPLATE)
position = kwargs[ATTR_POSITION] position = kwargs[ATTR_POSITION]
percentage_position = position percentage_position = position
position = self.find_in_range_from_percent(position, COVER_PAYLOAD) position = self.find_in_range_from_percent(position, COVER_PAYLOAD)
if set_position_template is not None: if template is not None:
position = set_position_template.async_render(parse_result=False, **kwargs) variables = {
"position": percentage_position,
"entity_id": self.entity_id,
"position_open": self._config[CONF_POSITION_OPEN],
"position_closed": self._config[CONF_POSITION_CLOSED],
"tilt_min": self._config[CONF_TILT_MIN],
"tilt_max": self._config[CONF_TILT_MAX],
}
position = template.async_render(parse_result=False, variables=variables)
mqtt.async_publish( mqtt.async_publish(
self.hass, self.hass,
@ -650,3 +692,29 @@ class MqttCover(MqttEntity, CoverEntity):
if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE): if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE):
position = max_range - position + offset position = max_range - position + offset
return position return position
def tilt_payload_received(self, _payload):
"""Set the tilt value."""
try:
payload = int(round(float(_payload)))
except ValueError:
_LOGGER.warning("Payload '%s' is not numeric", _payload)
return
if (
self._config[CONF_TILT_MIN] <= int(payload) <= self._config[CONF_TILT_MAX]
or self._config[CONF_TILT_MAX]
<= int(payload)
<= self._config[CONF_TILT_MIN]
):
level = self.find_percentage_in_range(payload)
self._tilt_value = level
self.async_write_ha_state()
else:
_LOGGER.warning(
"Payload '%s' is out of range, must be between '%s' and '%s' inclusive",
payload,
self._config[CONF_TILT_MIN],
self._config[CONF_TILT_MAX],
)

View File

@ -260,6 +260,45 @@ async def test_state_via_template(hass, mqtt_mock):
assert state.state == STATE_CLOSED assert state.state == STATE_CLOSED
async def test_state_via_template_and_entity_id(hass, mqtt_mock):
"""Test the controlling state via topic."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "state-topic",
"command_topic": "command-topic",
"qos": 0,
"value_template": '\
{% if value == "open" or value == "closed" %}\
{{ value }}\
{% else %}\
{{ states(entity_id) }}\
{% endif %}',
}
},
)
await hass.async_block_till_done()
state = hass.states.get("cover.test")
assert state.state == STATE_UNKNOWN
async_fire_mqtt_message(hass, "state-topic", "open")
async_fire_mqtt_message(hass, "state-topic", "invalid")
state = hass.states.get("cover.test")
assert state.state == STATE_OPEN
async_fire_mqtt_message(hass, "state-topic", "closed")
async_fire_mqtt_message(hass, "state-topic", "invalid")
state = hass.states.get("cover.test")
assert state.state == STATE_CLOSED
async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog):
"""Test the controlling state via topic with JSON value.""" """Test the controlling state via topic with JSON value."""
assert await async_setup_component( assert await async_setup_component(
@ -336,6 +375,47 @@ async def test_position_via_template(hass, mqtt_mock):
assert state.state == STATE_CLOSED assert state.state == STATE_CLOSED
async def test_position_via_template_and_entity_id(hass, mqtt_mock):
"""Test the controlling state via topic."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"position_topic": "get-position-topic",
"command_topic": "command-topic",
"qos": 0,
"position_template": '\
{% if state_attr(entity_id, "current_position") == None %}\
{{ value }}\
{% else %}\
{{ state_attr(entity_id, "current_position") + value | int }}\
{% endif %}',
}
},
)
await hass.async_block_till_done()
state = hass.states.get("cover.test")
assert state.state == STATE_UNKNOWN
async_fire_mqtt_message(hass, "get-position-topic", "10")
current_cover_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_POSITION
]
assert current_cover_position == 10
async_fire_mqtt_message(hass, "get-position-topic", "10")
current_cover_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_POSITION
]
assert current_cover_position == 20
async def test_optimistic_state_change(hass, mqtt_mock): async def test_optimistic_state_change(hass, mqtt_mock):
"""Test changing state optimistically.""" """Test changing state optimistically."""
assert await async_setup_component( assert await async_setup_component(
@ -712,7 +792,13 @@ async def test_position_update(hass, mqtt_mock):
assert current_cover_position == 22 assert current_cover_position == 22
async def test_set_position_templated(hass, mqtt_mock): @pytest.mark.parametrize(
"pos_template,pos_call,pos_message",
[("{{position-1}}", 43, "42"), ("{{100-62}}", 100, "38")],
)
async def test_set_position_templated(
hass, mqtt_mock, pos_template, pos_call, pos_message
):
"""Test setting cover position via template.""" """Test setting cover position via template."""
assert await async_setup_component( assert await async_setup_component(
hass, hass,
@ -726,7 +812,51 @@ async def test_set_position_templated(hass, mqtt_mock):
"position_open": 100, "position_open": 100,
"position_closed": 0, "position_closed": 0,
"set_position_topic": "set-position-topic", "set_position_topic": "set-position-topic",
"set_position_template": "{{100-62}}", "set_position_template": pos_template,
"payload_open": "OPEN",
"payload_close": "CLOSE",
"payload_stop": "STOP",
}
},
)
await hass.async_block_till_done()
await hass.services.async_call(
cover.DOMAIN,
SERVICE_SET_COVER_POSITION,
{ATTR_ENTITY_ID: "cover.test", ATTR_POSITION: pos_call},
blocking=True,
)
mqtt_mock.async_publish.assert_called_once_with(
"set-position-topic", pos_message, 0, False
)
async def test_set_position_templated_and_attributes(hass, mqtt_mock):
"""Test setting cover position via template and using entities attributes."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"position_topic": "get-position-topic",
"command_topic": "command-topic",
"position_open": 100,
"position_closed": 0,
"set_position_topic": "set-position-topic",
"set_position_template": '\
{% if position > 99 %}\
{% if state_attr(entity_id, "current_position") == None %}\
{{ 5 }}\
{% else %}\
{{ 23 }}\
{% endif %}\
{% else %}\
{{ 42 }}\
{% endif %}',
"payload_open": "OPEN", "payload_open": "OPEN",
"payload_close": "CLOSE", "payload_close": "CLOSE",
"payload_stop": "STOP", "payload_stop": "STOP",
@ -742,8 +872,85 @@ async def test_set_position_templated(hass, mqtt_mock):
blocking=True, blocking=True,
) )
mqtt_mock.async_publish.assert_called_once_with("set-position-topic", "5", 0, False)
async def test_set_tilt_templated(hass, mqtt_mock):
"""Test setting cover tilt position via template."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"position_topic": "get-position-topic",
"command_topic": "command-topic",
"tilt_command_topic": "tilt-command-topic",
"position_open": 100,
"position_closed": 0,
"set_position_topic": "set-position-topic",
"set_position_template": "{{position-1}}",
"tilt_command_template": "{{tilt_position+1}}",
"payload_open": "OPEN",
"payload_close": "CLOSE",
"payload_stop": "STOP",
}
},
)
await hass.async_block_till_done()
await hass.services.async_call(
cover.DOMAIN,
SERVICE_SET_COVER_TILT_POSITION,
{ATTR_ENTITY_ID: "cover.test", ATTR_TILT_POSITION: 41},
blocking=True,
)
mqtt_mock.async_publish.assert_called_once_with( mqtt_mock.async_publish.assert_called_once_with(
"set-position-topic", "38", 0, False "tilt-command-topic", "42", 0, False
)
async def test_set_tilt_templated_and_attributes(hass, mqtt_mock):
"""Test setting cover tilt position via template and using entities attributes."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"position_topic": "get-position-topic",
"command_topic": "command-topic",
"tilt_command_topic": "tilt-command-topic",
"position_open": 100,
"position_closed": 0,
"set_position_topic": "set-position-topic",
"set_position_template": "{{position-1}}",
"tilt_command_template": '\
{% if state_attr(entity_id, "friendly_name") != "test" %}\
{{ 5 }}\
{% else %}\
{{ 23 }}\
{% endif %}',
"payload_open": "OPEN",
"payload_close": "CLOSE",
"payload_stop": "STOP",
}
},
)
await hass.async_block_till_done()
await hass.services.async_call(
cover.DOMAIN,
SERVICE_SET_COVER_TILT_POSITION,
{ATTR_ENTITY_ID: "cover.test", ATTR_TILT_POSITION: 99},
blocking=True,
)
mqtt_mock.async_publish.assert_called_once_with(
"tilt-command-topic", "23", 0, False
) )
@ -2508,6 +2715,187 @@ async def test_position_via_position_topic_template_json_value(hass, mqtt_mock,
) in caplog.text ) in caplog.text
async def test_position_template_with_entity_id(hass, mqtt_mock):
"""Test position by updating status via position template."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "state-topic",
"command_topic": "command-topic",
"set_position_topic": "set-position-topic",
"position_topic": "get-position-topic",
"position_template": '\
{% if state_attr(entity_id, "current_position") != None %}\
{{ value | int + state_attr(entity_id, "current_position") }} \
{% else %} \
{{ value }} \
{% endif %}',
}
},
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "get-position-topic", "10")
current_cover_position_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_POSITION
]
assert current_cover_position_position == 10
async_fire_mqtt_message(hass, "get-position-topic", "10")
current_cover_position_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_POSITION
]
assert current_cover_position_position == 20
async def test_position_via_position_topic_template_return_json(hass, mqtt_mock):
"""Test position by updating status via position template and returning json."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "state-topic",
"command_topic": "command-topic",
"set_position_topic": "set-position-topic",
"position_topic": "get-position-topic",
"position_template": '{{ {"position" : value} | tojson }}',
}
},
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "get-position-topic", "55")
current_cover_position_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_POSITION
]
assert current_cover_position_position == 55
async def test_position_via_position_topic_template_return_json_warning(
hass, caplog, mqtt_mock
):
"""Test position by updating status via position template returning json without position attribute."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "state-topic",
"command_topic": "command-topic",
"set_position_topic": "set-position-topic",
"position_topic": "get-position-topic",
"position_template": '{{ {"pos" : value} | tojson }}',
}
},
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "get-position-topic", "55")
assert (
"Template (position_template) returned JSON without position attribute"
in caplog.text
)
async def test_position_and_tilt_via_position_topic_template_return_json(
hass, mqtt_mock
):
"""Test position and tilt by updating the position via position template."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "state-topic",
"command_topic": "command-topic",
"set_position_topic": "set-position-topic",
"position_topic": "get-position-topic",
"position_template": '\
{{ {"position" : value, "tilt_position" : (value | int / 2)| int } | tojson }}',
}
},
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "get-position-topic", "0")
current_cover_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_POSITION
]
current_tilt_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_TILT_POSITION
]
assert current_cover_position == 0 and current_tilt_position == 0
async_fire_mqtt_message(hass, "get-position-topic", "99")
current_cover_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_POSITION
]
current_tilt_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_TILT_POSITION
]
assert current_cover_position == 99 and current_tilt_position == 49
async def test_position_via_position_topic_template_all_variables(hass, mqtt_mock):
"""Test position by updating status via position template."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "state-topic",
"command_topic": "command-topic",
"set_position_topic": "set-position-topic",
"position_topic": "get-position-topic",
"tilt_command_topic": "tilt-command-topic",
"position_open": 99,
"position_closed": 1,
"tilt_min": 11,
"tilt_max": 22,
"position_template": "\
{% if value | int < tilt_max %}\
{{ tilt_min }}\
{% endif %}\
{% if value | int > position_closed %}\
{{ position_open }}\
{% endif %}",
}
},
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "get-position-topic", "0")
current_cover_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_POSITION
]
assert current_cover_position == 10
async_fire_mqtt_message(hass, "get-position-topic", "55")
current_cover_position = hass.states.get("cover.test").attributes[
ATTR_CURRENT_POSITION
]
assert current_cover_position == 100
async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock):
"""Test the controlling state via stopped state when no position topic.""" """Test the controlling state via stopped state when no position topic."""
assert await async_setup_component( assert await async_setup_component(
@ -2555,3 +2943,29 @@ async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock):
state = hass.states.get("cover.test") state = hass.states.get("cover.test")
assert state.state == STATE_CLOSED assert state.state == STATE_CLOSED
async def test_position_via_position_topic_template_return_invalid_json(
hass, caplog, mqtt_mock
):
"""Test position by updating status via position template and returning invalid json."""
assert await async_setup_component(
hass,
cover.DOMAIN,
{
cover.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "state-topic",
"command_topic": "command-topic",
"set_position_topic": "set-position-topic",
"position_topic": "get-position-topic",
"position_template": '{{ {"position" : invalid_json} }}',
}
},
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "get-position-topic", "55")
assert ("Payload '{'position': Undefined}' is not numeric") in caplog.text