mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Handle empty or missing state values for MQTT light entities using 'template' schema (#141177)
* check for empty or missing values when processing state messages for MQTT light entities using 'template' schema * normalise warning logs * add tests (one is still failing and I can't work out why) * fix test * improve test coverage after PR review * improve test coverage after PR review
This commit is contained in:
parent
fee152654d
commit
3951c2ea66
@ -62,6 +62,7 @@ from ..entity import MqttEntity
|
|||||||
from ..models import (
|
from ..models import (
|
||||||
MqttCommandTemplate,
|
MqttCommandTemplate,
|
||||||
MqttValueTemplate,
|
MqttValueTemplate,
|
||||||
|
PayloadSentinel,
|
||||||
PublishPayloadType,
|
PublishPayloadType,
|
||||||
ReceiveMessage,
|
ReceiveMessage,
|
||||||
)
|
)
|
||||||
@ -126,7 +127,9 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
|
|||||||
_command_templates: dict[
|
_command_templates: dict[
|
||||||
str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType]
|
str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType]
|
||||||
]
|
]
|
||||||
_value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]]
|
_value_templates: dict[
|
||||||
|
str, Callable[[ReceivePayloadType, ReceivePayloadType], ReceivePayloadType]
|
||||||
|
]
|
||||||
_fixed_color_mode: ColorMode | str | None
|
_fixed_color_mode: ColorMode | str | None
|
||||||
_topics: dict[str, str | None]
|
_topics: dict[str, str | None]
|
||||||
|
|
||||||
@ -203,73 +206,133 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
|
|||||||
@callback
|
@callback
|
||||||
def _state_received(self, msg: ReceiveMessage) -> None:
|
def _state_received(self, msg: ReceiveMessage) -> None:
|
||||||
"""Handle new MQTT messages."""
|
"""Handle new MQTT messages."""
|
||||||
state = self._value_templates[CONF_STATE_TEMPLATE](msg.payload)
|
state_value = self._value_templates[CONF_STATE_TEMPLATE](
|
||||||
if state == STATE_ON:
|
msg.payload,
|
||||||
|
PayloadSentinel.NONE,
|
||||||
|
)
|
||||||
|
if not state_value:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Ignoring message from '%s' with empty state value", msg.topic
|
||||||
|
)
|
||||||
|
elif state_value == STATE_ON:
|
||||||
self._attr_is_on = True
|
self._attr_is_on = True
|
||||||
elif state == STATE_OFF:
|
elif state_value == STATE_OFF:
|
||||||
self._attr_is_on = False
|
self._attr_is_on = False
|
||||||
elif state == PAYLOAD_NONE:
|
elif state_value == PAYLOAD_NONE:
|
||||||
self._attr_is_on = None
|
self._attr_is_on = None
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning("Invalid state value received")
|
_LOGGER.warning(
|
||||||
|
"Invalid state value '%s' received from %s",
|
||||||
|
state_value,
|
||||||
|
msg.topic,
|
||||||
|
)
|
||||||
|
|
||||||
if CONF_BRIGHTNESS_TEMPLATE in self._config:
|
if CONF_BRIGHTNESS_TEMPLATE in self._config:
|
||||||
try:
|
brightness_value = self._value_templates[CONF_BRIGHTNESS_TEMPLATE](
|
||||||
if brightness := int(
|
msg.payload,
|
||||||
self._value_templates[CONF_BRIGHTNESS_TEMPLATE](msg.payload)
|
PayloadSentinel.NONE,
|
||||||
):
|
)
|
||||||
self._attr_brightness = brightness
|
if not brightness_value:
|
||||||
else:
|
_LOGGER.debug(
|
||||||
_LOGGER.debug(
|
"Ignoring message from '%s' with empty brightness value",
|
||||||
"Ignoring zero brightness value for entity %s",
|
msg.topic,
|
||||||
self.entity_id,
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if brightness := int(brightness_value):
|
||||||
|
self._attr_brightness = brightness
|
||||||
|
else:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Ignoring zero brightness value for entity %s",
|
||||||
|
self.entity_id,
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Invalid brightness value '%s' received from %s",
|
||||||
|
brightness_value,
|
||||||
|
msg.topic,
|
||||||
)
|
)
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
_LOGGER.warning("Invalid brightness value received from %s", msg.topic)
|
|
||||||
|
|
||||||
if CONF_COLOR_TEMP_TEMPLATE in self._config:
|
if CONF_COLOR_TEMP_TEMPLATE in self._config:
|
||||||
try:
|
color_temp_value = self._value_templates[CONF_COLOR_TEMP_TEMPLATE](
|
||||||
color_temp = self._value_templates[CONF_COLOR_TEMP_TEMPLATE](
|
msg.payload,
|
||||||
msg.payload
|
PayloadSentinel.NONE,
|
||||||
|
)
|
||||||
|
if not color_temp_value:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Ignoring message from '%s' with empty color temperature value",
|
||||||
|
msg.topic,
|
||||||
)
|
)
|
||||||
self._attr_color_temp_kelvin = (
|
else:
|
||||||
int(color_temp)
|
try:
|
||||||
if self._color_temp_kelvin
|
self._attr_color_temp_kelvin = (
|
||||||
else color_util.color_temperature_mired_to_kelvin(int(color_temp))
|
int(color_temp_value)
|
||||||
if color_temp != "None"
|
if self._color_temp_kelvin
|
||||||
else None
|
else color_util.color_temperature_mired_to_kelvin(
|
||||||
)
|
int(color_temp_value)
|
||||||
except ValueError:
|
)
|
||||||
_LOGGER.warning("Invalid color temperature value received")
|
if color_temp_value != "None"
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Invalid color temperature value '%s' received from %s",
|
||||||
|
color_temp_value,
|
||||||
|
msg.topic,
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
CONF_RED_TEMPLATE in self._config
|
CONF_RED_TEMPLATE in self._config
|
||||||
and CONF_GREEN_TEMPLATE in self._config
|
and CONF_GREEN_TEMPLATE in self._config
|
||||||
and CONF_BLUE_TEMPLATE in self._config
|
and CONF_BLUE_TEMPLATE in self._config
|
||||||
):
|
):
|
||||||
try:
|
red_value = self._value_templates[CONF_RED_TEMPLATE](
|
||||||
red = self._value_templates[CONF_RED_TEMPLATE](msg.payload)
|
msg.payload,
|
||||||
green = self._value_templates[CONF_GREEN_TEMPLATE](msg.payload)
|
PayloadSentinel.NONE,
|
||||||
blue = self._value_templates[CONF_BLUE_TEMPLATE](msg.payload)
|
)
|
||||||
if red == "None" and green == "None" and blue == "None":
|
green_value = self._value_templates[CONF_GREEN_TEMPLATE](
|
||||||
self._attr_hs_color = None
|
msg.payload,
|
||||||
else:
|
PayloadSentinel.NONE,
|
||||||
self._attr_hs_color = color_util.color_RGB_to_hs(
|
)
|
||||||
int(red), int(green), int(blue)
|
blue_value = self._value_templates[CONF_BLUE_TEMPLATE](
|
||||||
)
|
msg.payload,
|
||||||
|
PayloadSentinel.NONE,
|
||||||
|
)
|
||||||
|
if not red_value or not green_value or not blue_value:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Ignoring message from '%s' with empty color value", msg.topic
|
||||||
|
)
|
||||||
|
elif red_value == "None" and green_value == "None" and blue_value == "None":
|
||||||
|
self._attr_hs_color = None
|
||||||
self._update_color_mode()
|
self._update_color_mode()
|
||||||
except ValueError:
|
else:
|
||||||
_LOGGER.warning("Invalid color value received")
|
try:
|
||||||
|
self._attr_hs_color = color_util.color_RGB_to_hs(
|
||||||
|
int(red_value), int(green_value), int(blue_value)
|
||||||
|
)
|
||||||
|
self._update_color_mode()
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning("Invalid color value received from %s", msg.topic)
|
||||||
|
|
||||||
if CONF_EFFECT_TEMPLATE in self._config:
|
if CONF_EFFECT_TEMPLATE in self._config:
|
||||||
effect = str(self._value_templates[CONF_EFFECT_TEMPLATE](msg.payload))
|
effect_value = self._value_templates[CONF_EFFECT_TEMPLATE](
|
||||||
if (
|
msg.payload,
|
||||||
effect_list := self._config[CONF_EFFECT_LIST]
|
PayloadSentinel.NONE,
|
||||||
) and effect in effect_list:
|
)
|
||||||
self._attr_effect = effect
|
if not effect_value:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Ignoring message from '%s' with empty effect value", msg.topic
|
||||||
|
)
|
||||||
|
elif (effect_list := self._config[CONF_EFFECT_LIST]) and str(
|
||||||
|
effect_value
|
||||||
|
) in effect_list:
|
||||||
|
self._attr_effect = str(effect_value)
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning("Unsupported effect value received")
|
_LOGGER.warning(
|
||||||
|
"Unsupported effect value '%s' received from %s",
|
||||||
|
effect_value,
|
||||||
|
msg.topic,
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _prepare_subscribe_topics(self) -> None:
|
def _prepare_subscribe_topics(self) -> None:
|
||||||
|
@ -1545,3 +1545,109 @@ async def test_rgb_value_template_fails(
|
|||||||
"TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template"
|
"TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template"
|
||||||
in caplog.text
|
in caplog.text
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"hass_config",
|
||||||
|
[
|
||||||
|
help_custom_config(
|
||||||
|
light.DOMAIN,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"effect_list": ["rainbow", "colorloop"],
|
||||||
|
"state_topic": "test-topic",
|
||||||
|
"state_template": "{{ value_json.state }}",
|
||||||
|
"brightness_template": "{{ value_json.brightness }}",
|
||||||
|
"color_temp_template": "{{ value_json.color_temp }}",
|
||||||
|
"red_template": "{{ value_json.color.red }}",
|
||||||
|
"green_template": "{{ value_json.color.green }}",
|
||||||
|
"blue_template": "{{ value_json.color.blue }}",
|
||||||
|
"effect_template": "{{ value_json.effect }}",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_state_templates_ignore_missing_values(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test that rendering of MQTT value template ignores missing values."""
|
||||||
|
await mqtt_mock_entry()
|
||||||
|
|
||||||
|
# turn on the light
|
||||||
|
async_fire_mqtt_message(hass, "test-topic", '{"state": "on"}')
|
||||||
|
state = hass.states.get("light.test")
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes.get("rgb_color") is None
|
||||||
|
assert state.attributes.get("brightness") is None
|
||||||
|
assert state.attributes.get("color_temp_kelvin") is None
|
||||||
|
assert state.attributes.get("effect") is None
|
||||||
|
|
||||||
|
# update brightness and color temperature (with no state)
|
||||||
|
async_fire_mqtt_message(
|
||||||
|
hass, "test-topic", '{"brightness": 255, "color_temp": 145}'
|
||||||
|
)
|
||||||
|
state = hass.states.get("light.test")
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes.get("rgb_color") == (
|
||||||
|
246,
|
||||||
|
244,
|
||||||
|
255,
|
||||||
|
) # temp converted to color
|
||||||
|
assert state.attributes.get("brightness") == 255
|
||||||
|
assert state.attributes.get("color_temp_kelvin") == 6896
|
||||||
|
assert state.attributes.get("effect") is None
|
||||||
|
assert state.attributes.get("xy_color") == (0.317, 0.317) # temp converted to color
|
||||||
|
assert state.attributes.get("hs_color") == (
|
||||||
|
251.249,
|
||||||
|
4.253,
|
||||||
|
) # temp converted to color
|
||||||
|
|
||||||
|
# update color
|
||||||
|
async_fire_mqtt_message(
|
||||||
|
hass, "test-topic", '{"color": {"red": 255, "green": 128, "blue": 64}}'
|
||||||
|
)
|
||||||
|
state = hass.states.get("light.test")
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes.get("rgb_color") == (255, 128, 64)
|
||||||
|
assert state.attributes.get("brightness") == 255
|
||||||
|
assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority
|
||||||
|
assert state.attributes.get("effect") is None
|
||||||
|
|
||||||
|
# update brightness
|
||||||
|
async_fire_mqtt_message(hass, "test-topic", '{"brightness": 128}')
|
||||||
|
state = hass.states.get("light.test")
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes.get("rgb_color") == (255, 128, 64)
|
||||||
|
assert state.attributes.get("brightness") == 128
|
||||||
|
assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority
|
||||||
|
assert state.attributes.get("effect") is None
|
||||||
|
|
||||||
|
# update effect
|
||||||
|
async_fire_mqtt_message(hass, "test-topic", '{"effect": "rainbow"}')
|
||||||
|
state = hass.states.get("light.test")
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes.get("rgb_color") == (255, 128, 64)
|
||||||
|
assert state.attributes.get("brightness") == 128
|
||||||
|
assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority
|
||||||
|
assert state.attributes.get("effect") == "rainbow"
|
||||||
|
|
||||||
|
# invalid effect
|
||||||
|
async_fire_mqtt_message(hass, "test-topic", '{"effect": "invalid"}')
|
||||||
|
state = hass.states.get("light.test")
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes.get("rgb_color") == (255, 128, 64)
|
||||||
|
assert state.attributes.get("brightness") == 128
|
||||||
|
assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority
|
||||||
|
assert state.attributes.get("effect") == "rainbow"
|
||||||
|
|
||||||
|
# turn off the light
|
||||||
|
async_fire_mqtt_message(hass, "test-topic", '{"state": "off"}')
|
||||||
|
state = hass.states.get("light.test")
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
assert state.attributes.get("rgb_color") is None
|
||||||
|
assert state.attributes.get("brightness") is None
|
||||||
|
assert state.attributes.get("color_temp_kelvin") is None
|
||||||
|
assert state.attributes.get("effect") is None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user